OnCallReady

BashLinux · 2 min read

Why set -e doesn't catch a failure in foo | bar (and the other gaps)

set -euo pipefail is the right header, but it has holes - pipelines, conditions, && chains and local x=$(cmd). What each one misses and how to close it.

Every bash script you share should start with:

bash
#!/usr/bin/env bash
set -euo pipefail
  • -e exits on the first failing command;
  • -u makes an unset variable an error instead of an empty string;
  • -o pipefail makes a pipeline fail when any part fails.

It's the right default, but it isn't a guarantee.

The pipeline gap

Without pipefail, a pipeline's exit status is the status of its last command:

bash
set -e
build | tee build.log     # build fails...
echo "deployed"           # ...and this still runs: tee succeeded

set -e only sees the status of the whole pipeline, and that's tee's 0. With set -o pipefail, the pipeline returns the last non-zero status in it, and the script stops. In a container's entrypoint script, a masked failure like this is how you get a container that restarts with a confusing exit code, and an entrypoint that doesn't exec its app never passes SIGTERM on.

Conditions and && / || chains

-e is deliberately switched off where a failure is part of a test:

bash
if grep -q needle file; then ...        # a failing grep is just "false"
check_config && restart_service         # check_config failing does not exit

That's by design. But it also means a function called inside an if runs its whole body with -e off.

local x=$(cmd) swallows the exit code

bash
f() {
  local out=$(false)     # the status of this line is local's: 0
  echo "still here"
}

local is itself a command, and it succeeds, so the failure of $(false) is lost. Declare first, assign second:

bash
f() {
  local out
  out=$(false)           # now -e sees the failure
}

shellcheck flags this as SC2155. Running shellcheck on every script catches this and most quoting bugs. CI scripts are where these gaps hurt most, and where a secret can end up in the build log.

The short version for an interview

"set -e doesn't catch foo | bar because a pipeline's status is the last command's. Add pipefail. It also skips conditions and &&/|| lists, and local x=$(cmd) hides the failure because local returns 0. Split the declaration from the assignment."

OnCallReady is free, with no ads and no tracking. RSS · All posts