Bash strict mode, explained

· shell bash

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 failed cd straight 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 legendary rm -rf "$DIR/" when DIR was 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 | true is 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 && other won't exit on cmd failing, it just runs the right side or not.
  • Commands inside if, while, and until conditions are exempt by design; that's how those constructs read exit codes.
  • A function called in a condition loses -e for its whole body, which is the one that genuinely surprises people. A function that would normally abort runs to completion when you call it as if 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.