Shlok Parida

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

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.