07 February 2021

Safer Bash: avoid nesting

In the past three blog posts (set -e, set -u, set -o pipefail), I have described the header I put as the first line of all my Bash scripts and why you probably should get into that habit too. As a reminder, here is the full line:

set -euo pipefail

Together, these three options give you a pretty good (relative to the out-of-the-box Bash experience, that is) safety net, in that you can expect most issues with your script to stop the script with an error.

There is, however, a common Bash pattern that can still result in uncaught errors. Let's start with an example:

$ cat subshell_failure.sh
set -euo pipefail

echo "Here is the current date: $(dat)."
echo "Success!"
$ bash subshell_failure.sh
subshell_failure.sh: line 3: dat: command not found
Here is the current date: .
Success!
$

The issue here is related to why we need pipefail in addition to errexit: we can think of errexit as checking the return code of each command before moving on to the next, but this behaviour, however, is not recursive.

In this case, while building the string does not succeed, that happens in a subcommand, and that subcommand, while failed, still returned a string. That string is then passed to the echo command, and the echo command succeeds, therefore avoiding detection by errexit.

I am not aware of any flag that can be set to catch this case. Therefore, the solution is to avoid it in the first place, i.e. to not nest commands further than you absolutely need to.

A subshell substitution on its own will correctly report an error (and thus trigger errexit if it is set):

$ cat subshell_caught.sh
set -euo pipefail

CURRENT_DATE=$(dat)

echo "Here is the current date: $CURRENT_DATE."
echo "Success!"
$ bash subshell_caught.sh
subshell_caught.sh: line 3: dat: command not found
$

Extracting complex expressions to a named variable is good practice in general in any programming language, but in Bash, you now have an extra reason to do it.

Tags: bash unix