Search code examples
bashshellscriptinglogic

bash, logic of: a && b && c


One thing I don't get with this operators is when I use two of them in sequence.

What I mean by that:

% true && echo "problem" 
problem

% echo $?
0

So far, so good. true returns "error" (exit status 1) and echo "problem" returns 0, so logical AND operation result must be 0.

% true && echo "problem" || echo "exit"
problem

OK, that's a surprise: since true && echo "problem" results in 0, || should also evaluate echo "exit", since after all right-hand operand of || might be true and so the result of this logical OR might be true.

Now:

% true && echo "problem" && echo "exit"
problem
exit

This is also surprising: after all since true && echo "problem" returns zero, the lazy && operator should not evaluate echo "exit" since the result of logical AND must be zero anyway.

Why is the behavior of last two examples opposite to what I intuitively expect?

P.S. This is opposite of Python behavior:

% python
>>> def pp():
...     print "problem"
... 

>>> def pe():
...     print "exit"

>>> True and pp() and pe()
problem

>>> True and pp() or pe()
problem
exit

Solution

  • You're misunderstanding success/failure statuses and also what && and || mean in this context in bash.

    An exit status of success is 0, while failure is non-zero so when you say true returns "error" (exit status 1), no it doesn't. Also, nothing in shell "returns" anything. Scripts and functions produce output and have an exit status - using the word "return" leads to confusion over which of those 2 separate things you mean. For example if we define this function:

    foo() {
        echo "hello"
        return 7
    }
    

    and then use it to populate a variable:

    var=$(foo)
    $ echo "$var"
    hello
    

    did foo() "return" hello or did foo() "return" 7? The best answer is no, it didn't "return" either - it output hello and exited with status 7.

    Although there's a poorly-named "return" keyword there what that REALLY is producing is an exit status for the function, the same as if you had a shell script that was just:

    echo "hello"
    exit 7
    

    and you can see that if you test it ($? always holds the exit status of the most recently run command):

    foo() {
        echo "hello"
        return 7
    }
    
    $ foo
    hello
    
    $ echo "$?"
    7
    

    I assume the shell creators chose "return" for the function keyword because "exit" already meant "exit from the running process" but IMHO that made things confusing, though I don't have a better suggestion and even if I did that ship has sailed long ago. If you read return 7 as set the exit status to 7 then return from the function without assuming the function is actually "returning" anything then you'd be right.

    Note also that the function is outputting "hello" - that's also not a "return" but if you use it as var=$(foo) then var ends up containing hello so then some people incorrectly refer to that as a "return" too since in other languages like C if you wrote var=foo():

    char *foo() {
        return "hello"
    }
    var=foo()
    

    then var would contain the argument that was given to return in the function but that is just not the same semantics as shell where, unlike the similar C code above, var=$(foo) sets var to the output from foo(), not the "return" (actually exit status) from foo().

    So - the function above doesn't actually "return" anything, it outputs hello and exits with status 7.

    So here's what your command line true && echo "problem" || echo "exit" actually does:

    • true = output nothing and exit with status 0 (success)
    • echo "problem" - output problem and exit with status 0 (success)
    • echo "exit" - output exit and exit with status 0 (success)

    Now, what do && and || mean? What they really are is shorthand for if statements:

    • && foo = if the previously run command exited success then execute foo
    • || foo = if the previously run command exited failure then execute foo

    So a command line like:

    cmdA && cmdB || cmdC
    

    in terms of success/fail status should be read as:

    cmdA
    ret=$?
    if (( ret == 0 )); then
        cmdB
        ret=$?
    fi
    if (( ret != 0 )); then
        cmdC
        ret=$?
    fi
    ( exit "$ret" )
    

    We need the ret temp variable because the if itself has an exit status that'd overwrite $?. So cmdC will get called if cmdA exits with a failure status, but it'll also get called if cmdA succeeded and then cmdB exited with a failure status. At the end of cmdA && cmdB || cmdC the exit status as stored in $? will simply be the exit status of whichever command ran last, it will not, for example, be the product of boolean arithmetic on all of the exit statuses of all the commands that ran as apparently suggested in the question might be the case.

    Note also that what that should NOT be read as is what you may intuitively have expected if you thought of && ... || ... as a ternary expression, which trips many people up, especially since that || is typically an error leg whose incorrect placement may escape your code inspectors/testers notice:

    cmdA
    if (( $? == 0 )); then
        cmdB
    else
        cmdC
    fi
    

    Given the above, here's what your command lines actually mean:

    1. true && echo "problem" || echo "exit"
    true
    ret=$?
    if (( ret == 0 )); then
        echo "problem"
        ret=$?
    fi
    if (( ret != 0 )); then
        echo "exit"
        ret=$?
    fi
    ( exit "$ret" )
    
    1. true && echo "problem" && echo "exit"
    true
    ret=$?
    if (( ret == 0 )); then
        echo "problem"
        ret=$?
    fi
    if (( ret == 0 )); then
        echo "exit"
        ret=$?
    fi
    ( exit "$ret" )
    

    and if what you WANTED to have happen instead of "2" above was actually:

    true
    if (( $? == 0 )); then
        echo "problem"
    else
        echo "exit"
    fi
    

    then you should write that code or similar instead of using &&s and ||s (e.g. as 1 line you could write if true; then echo "problem"; else echo "exit"; fi) so you don't get unexpected output if you reach the echo "problem" leg and it fails for some reason thereby causing you to afterwards fall into the echo "exit" leg (unlikely with just echo but very possible with other commands).