Bash – Difference Between [[ expr1 || expr2 ]] and [[ expr1 ]] || [[ expr2 ]]

bash

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 ]].

Best Answer

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.

Related Question