bash chained logical operator execution order, lazy evaluation

I don’t get this:

script: WORKDIR/sh/script.sh

[ -e filename ]          
&& echo filename         
|| [ -e ../filename ]    
&& echo ../filename      
|| { echo 'ERROR: failed to find "filename"' 1>&2 ; exit -1; }

output:

$ cd WORKDIR/sh
$ ./script.sh
../filename

$ cd WORKDIR
$ sh/script.sh
filename
../filename      # <---- WHY????

My thoughts:

1

[ -e filename ]           -> false
&&                         -> skip this, it is already false
   echo filename          -> don't even try
|| [ -e ../filename ]     -> true
&& echo ../filename       -> true
||                         -> already true, skip the rest
   { echo 'ERROR: failed to find "filename"' 1>&2 ; exit -1; }

2

[ -e filename ]           -> true
&& echo filename          -> true
||                         -> already true, skip the rest
   [ -e ../filename ]    
&& echo ../filename      
|| { echo 'ERROR: failed to find "filename"' 1>&2 ; exit -1; }

version:

bash --version
GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
Asked By: Zoltan K.

||

I think you are hoping that bash does this:

(A && B) || (C && D) || E

but it actually does this

(A && B || C) && D || E

Where D executes if either B or C succeeds.

Add more grouping:

[ -e filename ] && echo filename || {
    [ -e ../filename ] && echo ../filename || { 
        echo 'ERROR: failed to find "filename"' 1>&2 
        exit -1
    }
}

or

{ [ -e filename ]    && echo filename   ; } || 
{ [ -e ../filename ] && echo ../filename; } || 
{ echo 'ERROR: failed to find "filename"' 1>&2 ;  exit -1; }

or use the very clear and readable if-elif-else style demonstrated by @Jesse_b.


Step-by-step, this is happening:

  1. filename is in current directory:

    [ -e filename ]       # test succeeds, status is now 0
    &&                     # status is zero, will execute this branch
    echo filename         # echo succeeds, status is now 0
    ||                     # status is zero, do not execute
    [ -e ../filename ]    # not executed
    &&                     # status is zero, will execute this branch
    echo ../filename      # echo succeeds, status is now 0
    ||                     # status is zero, do not execute
    { echo; exit; }        # not executed
    
  2. filename is in parent directory:

    [ -e filename ]       # test fails, status is now 1
    &&                     # status is non-zero, do not execute this branch
    echo filename         # not executed
    ||                     # status is non-zero, will execute this branch
    [ -e ../filename ]    # test succeeds, status is now 0
    &&                     # status is zero, will execute this branch
    echo ../filename      # echo succeeds, status is now 0
    ||                     # status is zero, do not execute
    { echo; exit; }        # not executed
    
Answered By: glenn jackman

&& and || have equal precedence, so:

When a command passes, it will look for the next && and execute it, even if it is not the directly adjacent operator. You should never use more than one of these operators in a single command list. If more than one is needed you use an if/then construct.

$ true && true || echo yes && echo no
no

This is very much different than:

if true; then 
  true 
else 
  echo yes && echo no
fi

$ if true; then true; else echo yes && echo no; fi
$

Or:

$ true && false || echo yes && echo no
yes
no
$ if true; then false; else echo yes && echo no; fi
$

I would write your construct as:

if [ -e filename ]; then
    echo filename
elif [ -e ../filename ]; then
    echo ../filename
else
    echo 'ERROR: failed to find "filename"' >&2
    exit -1
fi
Answered By: jesse_b
Categories: Answers Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.