set -euo pipefail is not a seatbelt
Every serious shell script starts with it, and it is worth knowing precisely what it buys, because the gap between what people think it does and what it does is where the outages live.
set -euo pipefail
-eexit if a command fails-uerror on an unset variable-o pipefaila pipeline fails if any element fails, not just the last
All three are worth having. None of them makes a script safe.
Where -e does not fire
-e is suppressed wherever the shell is already inspecting an exit status. That means the failure is invisible in all of these:
if grep -q thing file; then ... # tested, so no exit
cmd || true # explicitly swallowed
cmd && other # left side is tested
! cmd # negated
while cmd; do ... # tested
The one that catches people is a function called in a test context. -e is disabled for the entire dynamic extent of the call:
check() {
rm /tmp/nonexistent # fails
echo "still here" # runs anyway
}
if check; then echo "fine"; fi
check does not abort at the rm. It runs to the end, returns the status of echo, which is zero, and prints fine. The failure vanished completely.
Where -e fires when you did not want it
count=$(grep -c thing file)
grep exits 1 when it matches nothing. That is not an error, it is an answer — but -e exits. Write it as:
count=$(grep -c thing file || true)
The same applies to diff (1 means "differences found"), and to any tool that uses exit status as information rather than as failure.
pipefail and early readers
set -o pipefail
producer | head -1
head reads one line and exits. producer keeps writing, gets SIGPIPE, and dies with status 141. pipefail propagates that, so the pipeline fails, and -e exits your script — for a pipeline that did exactly what you asked.
This is not exotic. ... | head, ... | grep -q, ... | read -r first are all this shape.
trap for cleanup, and the one flag that makes it work
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
EXIT fires on normal exit, on -e exit, and on most signals. Use it for anything you created — the alternative is a script with four exit paths and cleanup on two of them.
If you also want an ERR trap for diagnostics, note that ERR is not inherited by functions, subshells, or command substitutions unless you add -E:
set -Eeuo pipefail
trap 'echo "failed at line $LINENO" >&2' ERR
Without -E your handler silently does not run in exactly the nested cases you most wanted it for.
Quoting is still the actual defence
None of this protects you from the classic:
rm -rf "$dir/" # $dir unset under -u is now an error. Good.
rm -rf $dir/ # unquoted: word splitting, globbing. Still bad.
-u catches the unset case, which is real value. It does not catch a variable that is set to something with a space in it. Quote every expansion, use "$@" never $@, and prefer [[ ]] over [ ] in bash.
The honest summary
Use set -Eeuo pipefail. Treat it as a smoke alarm, not a sprinkler: it catches the careless failures and tells you nothing about the ones that matter most. Anything doing real damage — a deploy, an rm -rf, a migration — deserves an explicit status check that you wrote on purpose.