Precedence of the shell logical operators &&, ||

I am trying to understand how the logical operator precedence works in bash. For example, I would have expected, that the following command does not echo anything.

true || echo aaa && echo bbb

However, contrary to my expectation, bbb gets printed.

Can somebody please explain, how can I make sense of compounded && and || operators in bash?

Asked By: Martin Vegter

||

In many computer languages, operators with the same precedence are left-associative. That is, in the absence of grouping structures, leftmost operations are executed first. Bash is no exception to this rule.

This is important because in Bash and other shells && and || have the same precedence. This is different from most other programming languages which usually give && a higher precedence than ||.

So what happens in your example is that the leftmost operation (||) is carried out first:

true || echo aaa

Since true is obviously true, the || operator short-circuits and the whole statement is considered true without the need to evaluate echo aaa as you would expect. Now it remains to do the rightmost operation:

(...) && echo bbb

Since the first operation evaluated to true (i.e. had a 0 exit status), it’s as if you’re executing

true && echo bbb

so the && will not short-circuit, which is why you see bbb echoed.

You would get the same behavior with

false && echo aaa || echo bbb

Notes based on the comments

  • You should note that the left-associativity rule is only followed when both operators have the same precedence. This is not the case when you use these operators in conjunction with keywords such as [[...]] or ((...)) or use the -o and -a operators as arguments to the test or [ commands. In such cases, AND (&& or -a) takes precedence over OR (|| or -o). Thanks to Stephane Chazelas’ comment for clarifying this point.

  • It seems that in C and C-like languages && has higher precedence than || which is probably why you expected your original construct to behave like

    true || (echo aaa && echo bbb). 
    

This is not the case with Bash, however, in which both operators have the same precedence, which is why Bash parses your expression using the left-associativity rule. Thanks to Kevin’s comment for bringing this up.

  • There might also be cases where all 3 expressions are evaluated. If the first command returns a non-zero exit status, the || won’t short circuit and goes on to execute the second command. If the second command returns with a zero exit status, then the && won’t short-circuit as well and the third command will be executed. Thanks to Ignacio Vazquez-Abrams’ comment for bringing this up.
Answered By: Joseph R.

If you want multiple things to depend on your condition, group them:

true || { echo aaa && echo bbb; }

That prints nothing, while

true && { echo aaa && echo bbb; }

prints both strings.


The reason this happens is a lot more simple than Joseph is making out. Remember what Bash does with || and &&. It’s all about the previous command’s return status. A literal way of looking at your raw command is:

( true || echo aaa ) && echo bbb

The first command (true || echo aaa) is exiting with 0.

$ true || echo aaa; echo $?
0
$ true && echo aaa; echo $?
aaa
0

$ false && echo aaa; echo $?
1
$ false || echo aaa; echo $?
aaa
0
Answered By: Oli

The && and || operators are not exact inline replacements for if-then-else. Though if used carefully, they can accomplish much the same thing.

A single test is straightforward and unambiguous…

[[ A == A ]]  && echo TRUE                          # TRUE
[[ A == B ]]  && echo TRUE                          # 
[[ A == A ]]  || echo FALSE                         # 
[[ A == B ]]  || echo FALSE                         # FALSE

However, attempting to add multiple tests may yield unexpected results…

[[ A == A ]]  && echo TRUE   || echo FALSE          # TRUE  (as expected)
[[ A == B ]]  && echo TRUE   || echo FALSE          # FALSE (as expected)
[[ A == A ]]  || echo FALSE  && echo TRUE           # TRUE  (as expected)
[[ A == B ]]  || echo FALSE  && echo TRUE           # FALSE TRUE   (huh?)

Why are both FALSE and TRUE echoed?

What’s happening here is that we’ve not realized that && and || are overloaded operators that act differently inside conditional test brackets [[ ]] than they do in the AND and OR (conditional execution) list we have here.

From the bash manpage (edited)…

Lists

A list is a sequence of one or more pipelines separated by one of the
operators ;, &, &&, or ││, and optionally terminated by one of ;, &,
or . Of these list operators, && and ││ have equal
precedence, followed by ; and &, which have equal precedence.

A sequence of one or more newlines may appear in a list instead of a
semicolon to delimit commands.

If a command is terminated by the control operator &, the shell
executes the command in the background in a subshell. The shell does
not wait for the command to finish, and the return status is 0.
Commands separated by a ; are executed sequentially; the shell waits
for each command to terminate in turn. The return status is the exit
status of the last command executed.

AND and OR lists are sequences of one of more pipelines separated by
the && and ││ control operators, respectively. AND and OR lists are
executed with left associativity.

An AND list has the form …
command1 && command2
Command2 is executed if, and only if, command1 returns an exit status of zero.

An OR list has the form …
command1 ││ command2
Command2 is executed if and only if command1 returns a non-zero exit status.

The return status of AND and OR lists
is the exit status of the last command executed in the list.

Returning to our last example…

[[ A == B ]]  || echo FALSE  && echo TRUE

[[ A == B ]]  is false

     ||       Does NOT mean OR! It means...
              'execute next command if last command return code(rc) was false'

 echo FALSE   The 'echo' command rc is always true
              (i.e. it successfully echoed the word "FALSE")

     &&       Execute next command if last command rc was true

 echo TRUE    Since the 'echo FALSE' rc was true, then echo "TRUE"

Okay. If that’s correct, then why does the next to last example echo anything at all?

[[ A == A ]]  || echo FALSE  && echo TRUE


[[ A == A ]]  is true

     ||       execute next command if last command rc was false.

 echo FALSE   Since last rc was true, shouldn't it have stopped before this?
                Nope. Instead, it skips the 'echo FALSE', does not even try to
                execute it, and continues looking for a `&&` clause.

     &&       ... which it finds here

 echo TRUE    ... so, since `[[ A == A ]]` is true, then it echos "TRUE"

The risk of logic errors when using more than one && or || in a command list is quite high.

Recommendations

A single && or || in a command list works as expected so is pretty safe. If it’s a situation where you don’t need an else clause, something like the following can be clearer to follow (the curly braces are required to group the last 2 commands) …

[[ $1 == --help ]]  && { echo "$HELP"; exit; }

Multiple && and || operators, where each command except for the last is a test (i.e. inside brackets [[ ]]), are usually also safe as all but the last operator behave as expected. The last operator acts more like a then or else clause.

Answered By: DocSalvager

I also got confused by this but here’s how I think about the way Bash reads your statement (as it reads the symbols left to right):

  1. Found symbol true. This will need to be evaluated once the end of the command is reached. At this point, don’t know if it has any arguments. Store command in execution buffer.
  2. Found symbol ||. Previous command is now complete, so evaluate it. Command (buffer) being executed: true. Result of evaluation: 0 (i.e. success). Store result 0 in “last evaluation” register. Now consider symbol || itself. This depends on the result of the last evaluation being non-zero. Checked “last evaluation” register and found 0. Since 0 is not non-zero, the following command does not need to be evaluated.
  3. Found symbol echo. Can ignore this symbol, because the following command did not need to be evaluated.
  4. Found symbol aaa. This is an argument to command echo (3), but since echo (3) did not need to be evaluated, it can be ignored.
  5. Found symbol &&. This depends on the result of the last evaluation being zero. Checked “last evaluation” register and found 0. Since 0 is zero, the following command does need to be evaluated.
  6. Found symbol echo. This command needs to be evaluated once the end of the command is reached, because the following command did need to be evaluated. Store command in execution buffer.
  7. Found symbol bbb. This is an argument to command echo (6). Since echo did need to be evaluated, add bbb to execution buffer.
  8. Reached end of line. Previous command is now complete and did need to be evaluated. Command (buffer) being executed: echo bbb. Result of evaluation: 0 (i.e. success). Store result 0 in “last evaluation” register.

And of course, the last step causes bbb to be echoed to the console.

Answered By: Kidburla

Maybe a neat example:

% true || true && false;echo $?
1
% true || (true && false);echo $?
0

true is a command that does nothing and has the exit status 0 meaning "true".

The content of the parentheses runs in a subshell. echo $? displays the exit status of the last command.

Moreover, the question is self-contradictory: || and && in the context of the question are not logical operators but command compositors. (Roughly, they belong to modal not boolean logic.)

If you are indeed interested in logical operators && and || on strings, occuring inside test [[...]], && precedes || so that for example

%  [[ 0 || 0 && '' ]];echo $? 
0
%  [[ (0 || 0) && '' ]];echo $?
1

A string is true if and only if it is not empty.

Abstractly of exit status:

% if [[ 0 || 0 && '' ]];then echo oui;else echo non;fi
oui
% if [[ (0 || 0) && '' ]];then echo oui;else echo non;fi
non

All this applies to zsh or bash.

Answered By: Pierre ALBARÈDE
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.