Search code examples
basheval

Conditional construct with parameter and eval


Consider the following code. If I remove the eval and instead just write if "$1" it doesn't work. Is it possible to get rid of the eval?

assert () {
  if eval "$1"      #Is it possible to get rid of the eval?
  then
    echo "Assertion OK:  \"$1\""
  else
    echo "Assertion FAILED:  \"$1\""
  fi
}

assert " [[ /tmp/a = /tmp/* ]]"

Solution

  • It is possible to create a useful assert function without using eval:

    function assert
    {
        local IFS   # Protect "$*" against an unusual IFS value in the caller
    
        if "$@"; then
            printf 'Assertion OK: "%s"\n' "$*"
        else
            printf 'Assertion FAILED: "%s"\n' "$*"
        fi
    }
    

    Example uses:

    assert [ -f /etc/passwd ]   # OK
    assert [ ! -f /etc/group ]  # FAILED
    
    var1='a b'
    var2='a b'
    var3='-c'
    assert [ "$var1" = "$var2" ]    # OK
    assert [ "$var2" = "$var3" ]    # FAILED
    
    assert grep -q '^guest:' /etc/passwd    # Maybe OK, maybe FAILED
    

    The assert function works when the command ($1) is a user-defined function (e.g. dostuff), a Bash built-in command (e.g. [ or test), or an external command (e.g. grep).

    Because of the way that Bash processes lines of code, the function does not work if the command is a Bash reserved word (aka "keyword"). See the accepted answer to What's the difference between shell builtin and shell keyword? for excellent relevant information. In particular, it covers important differences between [ ... ] and [[ ... ]]. The standard Bash documentation is poor in this area.

    [[ is a reserved word, so this does not work:

      assert [[ /tmp/a = '/tmp/*' ]]          # Error: '[[' command  not found
    

    Since Bash does special processing on the code inside [[ ... ]], I don't think it is possible to modify the assert function to make assert [[ ... work in all cases. One way to work around the problem, without using eval, is to use [ instead of [[ in cases where their functionality overlaps (file access testing, simple string comparisions, ...) and use special assert_* functions for special functionality of [[. For instance, this function tests (glob) pattern matching:

    function assert_match
    {
        local -r str=$1
        local -r pat=$2
    
        # shellcheck disable=SC2053 # (Bad warning about not quoting $pat)
        if [[ $str == $pat ]]; then
            printf 'assert_match OK: "%s" matches "%s"\n' "$str" "$pat"
        else
            printf 'assert_match FAILED: "%s" does not match "%s"\n' "$str" "$pat"
        fi
    }
    

    Example uses:

    assert_match /tmp/a '/tmp/*'    # match OK
    assert_match /tmp/a '/tmp/b*'   # match FAILED
    

    An assert_rematch function for regular expression matches ([[ ... =~ ... ]]) might also be useful.