Bash strict mode, explained
Every decent shell script opens with the same incantation. It's worth knowing what it buys you instead of cargo-culting it, because each flag changes how the script behaves in ways that occasionally surprise you.
set -euo pipefail
IFS=$'\n\t'
That first line is what people call "bash strict mode." Here is what each piece does and where it bites.
The flags, one at a time
-e— exit the moment any command returns non-zero. No more marching past a failedcdstraight into commands that now run in the wrong directory. This is the big one: it turns "the script kept going after an error and made a mess" into "the script stopped at the error."-u— treat the use of an unset variable as an error. This is what saves you from the legendaryrm -rf "$DIR/"whenDIRwas never set and the path collapses to/. Typos in variable names become loud failures instead of silent empty strings.-o pipefail— a pipeline fails if any stage fails, not only the last one. Without it,false | trueis considered success, and a broken first command in a pipe is invisible. With it, the pipeline reports the failure.
The IFS line, and why it matters
IFS=$'\n\t'
The default IFS (internal field separator) includes a space, so unquoted
expansions split on spaces — which is how a filename with a space in it silently
becomes two arguments. Setting IFS to just newline and tab makes word-splitting
behave on file lists and for loops over $(...) output. It is not part of the
classic one-liner but it prevents a whole category of "works until a path has a
space" bugs. (The real fix is still to quote your variables; this is a safety
net, not a substitute.)
The gotcha that bites everyone
Under -e, a command in a conditional is allowed to fail, but a command whose
result you use is not. This catches people constantly:
count=$(grep -c foo file) # grep finds nothing -> exit 1 -> script dies here
grep returns exit code 1 when there are zero matches. That's not an error, it's
information — but -e sees a non-zero exit and kills the script. Guard it:
count=$(grep -c foo file || true)
The same trap hits any command that uses exit codes to communicate (diff,
grep, cmp). When you only want the output, append || true.
The other gotcha: -e doesn't apply everywhere
-e is quietly disabled inside several contexts, and the rules are subtle. The
ones to remember:
- Anything to the left of
&&or||is exempt —cmd && otherwon't exit oncmdfailing, it just runs the right side or not. - Commands inside
if,while, anduntilconditions are exempt by design; that's how those constructs read exit codes. - A function called in a condition loses
-efor its whole body, which is the one that genuinely surprises people. A function that would normally abort runs to completion when you call it asif myfunc; then.
This isn't a reason to drop -e — it's a reason not to assume it's a force
field.
Trap for cleanup
Strict mode plus a trap gives you reliable cleanup even when the script exits early on an error:
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
# ... use "$tmp"; whatever happens, it gets removed on exit
trap ... EXIT runs on normal exit and on the -e-triggered abort, so your
temp files don't leak when something fails halfway through.
What it actually gives you
Strict mode does not make a script correct. It makes it fail loudly and early
instead of quietly doing the wrong thing — stopping at the first error rather than
cascading, and turning typos and empty variables into immediate failures. That's
almost always what you want from automation you won't be watching when it runs.
Add the || true guards where you legitimately expect non-zero exits, and the
mode stops fighting you.
Common problems
My script exits on a grep / diff that found nothing
That's -e reacting to a normal non-zero exit code. grep returns 1 on no
match, diff returns 1 on a difference — neither is an error. When you only want
the output, append || true: count=$(grep -c foo file || true).
unbound variable on a positional argument
Under -u, referencing $1 when no argument was passed aborts the script.
Supply a default with parameter expansion: name="${1:-default}", or check
explicitly with [ $# -ge 1 ] before using it.
set -e isn't stopping my function
-e is suppressed inside a function that's called as a condition
(if myfunc; then), and to the left of &&/||. The function runs to
completion even after a command inside it fails. If you need a hard stop there,
check the exit code yourself rather than relying on -e.