Search code examples
bashconditional-statementsarithmetic-expressions

How to make bash throw an error if -eq is used with non-numeric arguments inside [[ ... ]]?


I recently wrote a fairly complex bash script and during testing noticed a misbehavior. During analysis, I found that there was a typo where I accidentally used -eq instead of == in a conditional command, i.e. I had something like

[[ "$var1" -eq "$var2" ]] || { ... }

instead of

[[ "$var1" == "$var2" ]] || { ... }

Since in this case var1 and var2 could contain arbitrary strings, not only integer numbers, things went horribly wrong at that place. A quick investigation at the command line makes the problem even more clear:

root@cerberus:~/scripts# [[ '0' -eq '0' ]] && echo Equal
Equal
root@cerberus:~/scripts# [[ '0' -eq '1' ]] && echo Equal
root@cerberus:~/scripts# [[ 'A' -eq 'B' ]] && echo Equal
Equal

The last line was very surprising to me. From the bash manual (at the end of the page, emphasis mine):

arg1 OP arg2

OP is one of ‘-eq’, ‘-ne’, ‘-lt’, ‘-le’, ‘-gt’, or ‘-ge’. These arithmetic binary operators return true if arg1 is equal to, not equal to, less than, less than or equal to, greater than, or greater than or equal to arg2, respectively. Arg1 and arg2 may be positive or negative integers. When used with the [[ command, Arg1 and Arg2 are evaluated as arithmetic expressions (see Shell Arithmetic).

Since the last line in the code above uses -eq with the [[ command, 'A' and 'B' are evaluated as arithmetic expressions according to the citation above. The surprising thing to me was that this did not make bash throw an error, because obviously there is no reasonable method to turn 'A' or 'B' into an integer number.

I then tried to find out the rules which bash applies when evaluating arithmetic expressions. The respective chapter in the manual makes statements about some edge cases (e.g. expressions' expansions being null or unset), but there is nothing in it about the case where one expression's expansion is an arbitrary string which can't be evaluated arithmetically. [Note: I am aware that we shouldn't work with link-only references, but the respective sections are too long for a citation.]

Out of curiosity, I tried to find out to which arithmetic value something like 'A' actually evaluates. This turned out to be more difficult than expected, because bash actually refuses to do "normal" arithmetic expansion on strings, even if these strings could be evaluated arithmetically:

root@cerberus:~/scripts# echo $(( 0 ))
0
root@cerberus:~/scripts# echo $(( '0' ))
-bash: '0' : syntax error: operand expected (error token is "'0' ")
root@cerberus:~/scripts# echo $(( 'A' ))
-bash: 'A' : syntax error: operand expected (error token is "'A' ")

Since I'd like to cure the problem, not the symptoms, I gave up on trying to find out the rules which bash applies when arithmetically evaluating arbitrary strings, or why it throws errors when it does this during arithmetic expansion, but not during arithmetic evaluation in conjunction with [[.

Instead, I now would like to know whether there is an option in bash which makes it throw an error in any situation where it must evaluate an expression arithmetically, but where this is not possible because that expression just isn't arithmetic. If there is no such option, I would like to know whether this is possible at least for the arithmetic evaluation of the arguments to -eq, -ne (and so on inside) when these operators are used inside [[ ]].


Solution

  • -eq in [[ ... ]] evaluates its operands as arithmetic expressions, i.e. they behave like in ((...)). Variables with or without a dollar sign are expanded, but if their expansion is a valid identifier, the variable of that name is used to produce the value to compare.

    #!/bin/bash
    A=foo
    B=bar
    for foo in 0 1 ; do
        for bar in 0 1 ; do
            if [[ A -eq B ]] ; then
                echo "$A ($foo) eq $B ($bar)"
            else
                echo "$A ($foo) ne $B ($bar)"
            fi
        done
    done
    

    This expansion happens recursively

    #!/bin/bash
    a=1 b=a c=b d=c e=d f=e g=f h=g i=h j=i k=j l=k m=l
    n=m o=n p=o q=p r=q s=r t=s u=t v=u w=v x=w y=x z=y
    [[ z -eq 1 ]] && echo ok
    

    The recursion limit is 1024:

    #!/bin/bash
    (
        echo -n a=
        for var in {a..z}{a..z}{a..z} ; do
            echo $var
            echo -n $var=
        done
        echo 42
        echo '[[ a -eq 42 ]] && echo ok'
    ) | bash