jcmorrow

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.

  1. 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.
  2. &&: This is used at the statement level and it pretty much works how you would expect it to work. A && B means try to execute A and only if it executes with a successful exit code go on to execute B. Otherwise, it short-circuits such that if A fails then B 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."