Every bash script you share should start with:
#!/usr/bin/env bash
set -euo pipefail-eexits on the first failing command;-umakes an unset variable an error instead of an empty string;-o pipefailmakes 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:
set -e
build | tee build.log # build fails...
echo "deployed" # ...and this still runs: tee succeededset -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:
if grep -q needle file; then ... # a failing grep is just "false"
check_config && restart_service # check_config failing does not exitThat'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
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:
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."