Yet another bash footgun
Today I found a new (to me) footgun in bash scripting.
Bash has two common ways to halt execution if something goes wrong.
set -e
: This is set at the subshell level, and will tell bash that if any* statement exits with a non-zero exit code then it should stop executing the script.&&
: This is used at the statement level and it pretty much works how you would expect it to work.A && B
means try to executeA
and only if it executes with a successful exit code go on to executeB
. Otherwise, it short-circuits such that ifA
fails thenB
is never evaluated.
*The Footgun
set -e
has exceptions to when it will halt on a non-zero exit code. One of those exceptions is, from the bash manual pages:
The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test following the if or elif reserved words, part of any command executed in a && or ||‚ list except the command following the final && or ||, any command in a pipeline but the last, or if the command's return value is being inverted with !
Let's examine our example above in light of these caveats.
set -e
A && B
echo "Finished."
Even if A
exits with non-zero exit code this script will run all the way to the end and print Finished.
. Why? Well, the statement A && B
falls under the caveat emphasised above, which means that set -e
will only cause the script to fail if B
fails. But if A
fails then B
is never evaluated, meaning it can never cause set -e
to halt execution!
So what do I do?
I think &&
is useful when you need the precise semantics of saying "Run the next command on this line only if the previous one succeeds". But it also effectively disables set -e
for statements on that line before the last &&
, so it should be used carefully. If you just want every statement to have the ability to halt the whole script then using set -e
and putting everything on its own line is a more straight-forward option. If you really want to use &&
in combination with set -e
you can also do this:
set -e
A && B || false
echo "This will never print."