8

Consider two conditional expressions expr1 and expr2, for example $i -eq $j and $k -eq $l. We can write this in bash a number of ways. Here are two possibilities

[[ expr1 || expr2 ]]

[[ expr1 ]] || [[ expr2 ]]

I'm fairly sure I've seen recommendations here that the second should be preferred, but I can't find evidence to support this.

Here is a sample script that seems to demonstrate there is no difference:

for i in 0 1
do
  for j in 0 1
  do
    for k in 0 1
    do
      for l in 0 1
      do
        if [[ $i -eq $j || $k -eq $l ]]; then printf "1-yes\t"; else printf "1-no\t"; fi
        if [[ $i -eq $j ]] || [[ $k -eq $l ]]; then printf "2-yes\n"; else printf "2-no\n"; fi
      done
    done
  done
done

and output showing that both condition constructs produce the same result:

1-yes   2-yes
1-yes   2-yes
1-yes   2-yes
1-yes   2-yes
1-yes   2-yes
1-no    2-no
1-no    2-no
1-yes   2-yes
1-yes   2-yes
1-no    2-no
1-no    2-no
1-yes   2-yes
1-yes   2-yes
1-yes   2-yes
1-yes   2-yes
1-yes   2-yes

Is there any benefit to using one construct over the other?

For bonus points, same question but generalising to multiple conditions using || and &&. For example, [[ expr1 && expr2 || expr3 ]].

  • With respect to why recommendations of this type are often given wrt. [ or test (not [[), search for the OB tags (linked to the "obsolescent" definition) in the POSIX spec for test. In test, there are some pathological cases where it can be impossible to tell if ( or ) is meant to be syntax meaningful to the test command or a string to be tested, so people who ignore the obsolescence markers and use that syntax can actually need the otherwise-obsolete "x$foo" practice; with [[, that isn't a problem. – Charles Duffy Jul 5 at 22:07
7

I think the recommendation you saw was for POSIX sh and/or the test command which doubles as the [ command, rather than the [[ construct which appeared in ksh (thanks Stéphane Chazelas for the tip) and is also used for example in bash, zsh and some other shells.

In most languages, like C, when a clause is already known to be true or false, there's no need to evaluate the remainder parts depending on the operation: if true not after a logical or following it, if false not after a logical and, etc. This of course allows for example to stop when a pointer is NULL and not try to dereference it in the next clause.

But sh's [ expr1 -o expr2 ] construct (including bash's implementation) doesn't do this: it always evaluates both sides, when one would want only expr1 to be evaluated. This might have been done for compatibility with the test command implementation. On the other hand, sh's || and && do follow the usual principle: not evaluated if it won't change the result.

So the difference to note would be:

: > /tmp/effect #clear effect
if [ 1 -eq 1 -o $(echo 1; echo or-was-evaluated > /tmp/effect) -eq 1 ]; then
    echo true;
fi
cat /tmp/effect

which yields:

true
or-was-evaluated

Above, each [ could have been replaced with /usr/bin/[ which is an alias to the test command, used before [ was made built-in to shells.

While the two next constructs:

: > /tmp/effect #clear effect
if [ 1 -eq 1 ] || [ $(echo 1; echo or-was-evaluated > /tmp/effect) -eq 1 ]; then
    echo true;
fi
cat /tmp/effect

or

: > /tmp/effect #clear effect
if [[ 1 -eq 1 || $(echo 1; echo or-was-evaluated > /tmp/effect) -eq 1 ]]; then
    echo true;
fi
cat /tmp/effect

Will only yield true and leave effect empty: || behaves correctly, and [[ corrected this issue too.


UPDATE:

As @StéphaneChazelas commented, I missed several differences related to the initial question. I'll just put here the most important one (at least to me): priority of operators.

While the shell will not considere precedence:

if true || true && false; then
    echo true
else
    echo false
fi

yields (because there is no precedence and thus first true || true is evaluated, and then && false):

false

inside [[ ]] the && operator has precedence over ||:

if [[ 1 -eq 1 || 1 -eq 1 && 1 -eq 0 ]]; then
    echo true
else
    echo false
fi

yields (because 1 -eq 1 && 1 -eq 0 is grouped and is thus the 2nd member of ||):

true

at least for ksh, bash, zsh.

So [[ ]] has improved behaviour over both [ ] and direct shell logical operators.

  • 1
    [ x -o y ] doesn't have to evaluate both side. While GNU test or the [ builtin of bash do, you'd find with strace zsh -c '[ x -o -f /x ]' that [ doesn't try to stat() /x. Same with mksh. But it's true that -o and -a are severely broken, can't be used reliably and are deprecated by POSIX. – Stéphane Chazelas Jul 5 at 11:20
  • 1
    One difference is that inside [[...]], && has higher precedence than ||, while outside they have equal precedence. Compare ksh -c '[[ x || x && "" ]]' vs sh -c 'true || true && false' – Stéphane Chazelas Jul 5 at 11:24
  • 1
    Note that the standard [ / test utility doesn't have a == operator. The equality operator is = (note that inside ksh's [[...]], =/== are not equality operators, but pattern matching ones). – Stéphane Chazelas Jul 5 at 11:26
  • 1
    It's the other way round. && has precedence over || inside [[...]] (or ((...))) but not the && and || shell operators. [[ x || y && z ]] is x || (y && z) while x || y && z is (x || y) && z (operators evaluated left to right without precedence). Similar to how * has precedence over + (1 + 2 * 3 is 1 + (2 * 3)) but + and - have same precedence. – Stéphane Chazelas Jul 5 at 13:04
  • 1
    If && has precedence over ||, then it should be true || (true && false), instead of (true || true) && false – Prvt_Yadv Jul 5 at 13:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.