11

When dereferencing a variable in bash, you have to use $ sign. Nevertheless, it seems that the following is working just fine:

x=5
[[ x -gt 2 ]]

Can anybody explain this?

Edit: (more info)

What I mean is how and why the [[ ]] command is dereferencing my variable x without the $ sign. And yes, if x=1, the statement is evaluated to false (return status 1)

Guest
  • 119
  • 2
    What do you mean by "working just fine"? And does your assessment change if you do x=1 followed by [[ x -gt 2]]? – nohillside Dec 03 '18 at 21:01
  • I mean: How and why the [[ ]] command is dereferencing my variable x without the $ sign. And yes, if x=1, the statement is false (return status 1) – Guest Dec 03 '18 at 21:13

3 Answers3

9

The reason is that the -eq forces an arithmetic evaluation of the arguments.

An arithmetic operator: -eq, -gt, -lt, -ge, -le and -ne inside a [[ ]] (in ksh,zsh and bash) means to automatically expand variable names as in the c language, not need for a leading $.

  • For confirmation we must look into bash source code. The manual offers no direct confirmation.

    Inside test.c the processing of arithmetic operators fall into this function:

    arithcomp (s, t, op, flags)
    

    Where s and t are both operands. The operands are handed to this function:

    l = evalexp (s, &expok);
    r = evalexp (t, &expok);
    

    The function evalexp is defined inside expr.c, which has this header:

    /* expr.c -- arithmetic expression evaluation. */
    

    So, yes, both sides of an arithmetic operator fall (directly) into arithmetic expression evaluation. Directly, no buts, no ifs.


In practice, with:

 $ x=3

Both of this fail:

 $ [[ x = 4 ]] && echo yes || echo no
 no

 $ [[ x = 3 ]] && echo yes || echo no
 no

Which is correct, x is not being expanded and x is not equal to a number.

However:

 $ [[ x -eq 3 ]] && echo yes || echo no
 yes

 $ [[ x -eq 4 ]] && echo yes || echo no
 no

The variable named x gets expanded (even without a $).

This doesn't happen for a […] in zsh or bash (it does in ksh).


That is the same as what happens inside a $((…)):

 $ echo $(( x + 7 ))
 10

And, please understand that this is (very) recursive (except in dash and yash):

 $ a=b b=c c=d d=e e=f f=3
 $ echo "$(( a + 7 ))" 
 10

A

And quite risky:

 $ x='a[$(date -u)]'
 $ [[ x -eq 3 ]] && echo yes || echo no
 bash: Tue Dec  3 23:18:19 UTC 2018: syntax error in expression (error token is "Dec  3 23:18:19 UTC 2018")

The syntax error could be easily avoided:

 $ a=3; x='a[$(date -u >/dev/tty; echo 0)]'

 $ [[ x -eq 3 ]] && echo yes || echo no
 Tue Dec  4 09:02:06 UTC 2018
 yes

As the saying goes: sanitize your input

 $ [[ ${x//[^0-9]} -eq 3 ]] && echo yes || echo no
 no

end of


Both the (older) external /usr/bin/test (not the builtin test) and the still older and also external expr do not expand expressions only integers (and apparently, only decimal integers):

 $ /usr/bin/test "x" -eq 3
 /usr/bin/test: invalid integer ‘x’

 $ expr x + 3
 expr: non-integer argument
  • Interesting. It's not hard to tell how this is possible - being [[ a keyword, operators and operands are detected when the command is read and not after expansion. Thus [[ can treat -eq in a more smart way than, say, [. But what I wonder is: where can we find documentation about the logic bash uses to interpret compound commands? It doesn't look quite obvious to me and I'm apparently unable to find satisfactory explanations in man or info bash. – fra-san Dec 03 '18 at 23:55
  • Bash doesn't document this anywhere I can find. There is a kind of description in man ksh93: The following obsolete arithmetic comparisons are also permitted: exp1 -eq exp2. There is this text in the test section of man zshbuiltins arithmetic operators expect integer arguments rather than arithmetic expressions. Which confirms that some arguments are treated as arithmetic expressions by the test builtin under conditions non specified in this quote. I'll confirm with the source code ….… –  Dec 04 '18 at 01:03
7

The operands of the numerical comparisons -eq, -gt, -lt, -ge, -le and -ne are taken as arithmetic expressions. With some limitation, they still need to be single shell words.

The behaviour of variable names in arithmetic expression is described in Shell Arithmetic:

Shell variables are allowed as operands; parameter expansion is performed before the expression is evaluated. Within an expression, shell variables may also be referenced by name without using the parameter expansion syntax. A shell variable that is null or unset evaluates to 0 when referenced by name without using the parameter expansion syntax.

and also:

The value of a variable is evaluated as an arithmetic expression when it is referenced

But I can't actually find the part of the documentation where it's said that the numeric comparisons take arithmetic expressions. It's not described in Conditional Constructs under [[, nor is it described in Bash Conditional Expressions.

But, by experiment, it seems to work as said above.

So, stuff like this works:

a=6
[[ a -eq 6 ]] && echo y 
[[ 1+2+3 -eq 6 ]] && echo y
[[ "1 + 2 + 3" -eq 6 ]] && echo y

this too (the value of the variable is evaluated):

b='1 + 2 + 3'
[[ b -eq 6 ]] && echo y

But this doesn't; it's not a single shell word when the [[ .. ]] is parsed, so there's a syntax error in the conditional:

[[ 1 + 2 + 3 -eq 6 ]] && echo y

In other arithmetic contexts, there's no need for the expression to be without whitespace. This prints 999, as the brackets unambiguously delimit the arithmetic expression in the index:

a[6]=999; echo ${a[1 + 2 + 3]}

On the other hand, the = comparison is a pattern match, and doesn't involve arithmetic, nor the automatic variable expansion done in an arithmetic context (Conditional Constructs):

When the == and != operators are used, the string to the right of the operator is considered a pattern and matched according to the rules described below in Pattern Matching, as if the extglob shell option were enabled. The = operator is identical to ==.

So this is false since the strings are obviously different:

[[ "1 + 2 + 3" = 6 ]] 

as is this, even though the numerical values are the same:

[[ 6 = 06 ]] 

and here, too, the strings (x and 6) are compared, they're different:

x=6
[[ x = 6 ]]

This would expand the variable, though, so this is true:

x=6
[[ $x = 6 ]]
ilkkachu
  • 138,973
  • can't actually find the part of the documentation where it's said that the numeric comparisons take arithmetic expressions. The confirmation is in the code. –  Dec 04 '18 at 01:09
  • The closest thing is that the description of arg1 OP arg2 says that the args may be positive or negative integers, which I guess is supposed to imply that they're treated as arithmetic expressions. Confusingly, it also implies that they can't be zero. :) – Barmar Dec 05 '18 at 17:20
  • @Barmar, ehh, right. But that applies to the numeric comparisons in [ too, and there they aren't arithmetic expressions. Instead, Bash complains about non-integers. – ilkkachu Dec 05 '18 at 22:12
  • @ilkkachu [ is an external command, it doesn't have access to shell variables. It's often optimized with a built-in command, but it still behaves the same. – Barmar Dec 05 '18 at 22:31
  • @Barmar, what I meant was that the phrase "Arg1 and arg2 may be positive or negative integers." appears in Bash Conditional Expressions, and that list applies to [ just as well as [[. Even with [, the operands to -eq and friends are/have to be integers, so that description also applies. Taking "must be integers" to mean "are interpreted as arithmetic expressions" doesn't apply in both cases. (Probably at least partly due to [ acting like an ordinary command, as you say.) – ilkkachu Dec 05 '18 at 22:55
  • (Though at least ksh is funny here, ksh -c 'a=3; [ a -eq 3 ] && echo y' outputs y: the variable is expanded even within [.) – ilkkachu Dec 05 '18 at 22:56
3

Yes, your observation is correct, variable expansion is performed on expressions under double brackets [[ ]], so you don't need to put $ in front of a variable name.

This is explicitly stated in the bash manual:

[[ expression ]]

(...) Word splitting and pathname expansion are not performed on the words between the [[ and ]]; tilde expansion, parameter and variable expansion, arithmetic expansion, command substitution, process substitution, and quote removal are performed.

Notice that this is not the case of single-bracket version [ ], as [ is not a shell keyword (syntax), but rather a command (in bash it is builtin, other shells could use external, lined to test).

jimmij
  • 47,140
  • 1
    Thank you for replying.

    It seems that this is working only for numbers.

    x=city
    
    [[ $x == city ]] 
    
    

    This doesn't work without the $ sign.

    – Guest Dec 03 '18 at 21:35
  • 3
    It looks like there is more here: (x=1; [[ $x = 1 ]]; echo $?) returns 0, (x=1; [[ x = 1 ]]; echo $?) returns 1, i.e. parameter expansion is not performed on x when we compare strings. This behavior looks like arithmetic evaluation triggered by arithmetic expansion, i.e. what happens in (x=1; echo $((x+1))). (About arithmetic evaluation, man bash states that "Within an expression, shell variables may also be referenced by name without using the parameter expansion syntax). – fra-san Dec 03 '18 at 21:47
  • @fra-san Indeed, because -gt operator expects number so whole expression is re-evaluated as if inside (()), on the other hand == expects strings so instead pattern-matching function is triggered. I didn't dig into source code, but sounds reasonable. – jimmij Dec 03 '18 at 22:04
  • [ is a shell builtin in bash. – Nizam Mohamed Dec 03 '18 at 22:41
  • 1
    @NizamMohamed It is a builtin, but it's still not a keyword. – Kusalananda Dec 03 '18 at 23:08
  • I don't think that quote explicitly states anything about this. There's no word about arithmetic contexts there. Arithmetic expansion is $(( .. )), and it works just fine within [ .. ], too. Besides, not everything inside [[ .. ]] is taken as an arithmetic expression, it depends on the operator used. – ilkkachu Dec 03 '18 at 23:32
  • This is false: you don't need to put $ in front of a variable name inside [[…]]. Yes, you do, for almost everything except numeric operators -eq, -ne, -lt, -le, -gt, or -ge for both arguments on each side of the operator. –  Dec 03 '18 at 23:55