Many bash scripts open with `set -euo pipefail`. What does each of the three options change about how the script behaves, and why do teams make it the standard first line?
answer
- three letters, three different defaults
- abort, undefined, pipeline
- errexit, nounset, pipefail
- non-zero stops the script
- a floor, not a guarantee
basics
~20 sIn bash, set -e aborts the script at the first command that returns non-zero, set -u makes expanding an unset variable an error instead of an empty string, and set -o pipefail makes a pipeline report failure from any stage rather than only the last.
solid answer
~50 s`set -e` (`errexit`) tells bash to stop the script as soon as a command exits non-zero, instead of ploughing on with the next line. `set -u` (`nounset`) turns the expansion of a variable that was never assigned into a fatal error, so a typo like `rm -rf "$prefx/data"` fails loudly rather than expanding to nothing. `set -o pipefail` makes a pipeline's exit status the rightmost non-zero status of its stages, so a failure that is not in the last stage is still visible. Teams standardise on the line because the default shell behaviour is the opposite of what a deployment or CI script wants: by default a failing command is ignored, an undefined variable is an empty string, and a pipeline reports only its last command. It is a floor, not a guarantee — `set -e` has well-known contexts where it deliberately does not fire, so real scripts still check errors explicitly.
go deeper
Be able to say what each letter does in one sentence and where the line goes — immediately under the shebang, before any real work. Knowing that non-zero means failure in the shell is the foundation everything else rests on.
Explain the mechanics: which default each option overturns, that nounset fires at runtime rather than at parse time, and how to opt out for one command with || true or one expansion with ${VAR:-} instead of disabling the option wholesale.
Show that you treat the line as a floor. Name at least one context where set -e is suspended, point out that it produces no diagnostic when it aborts, and describe the explicit error handling you add on top for a script that runs unattended in CI or cron.
Own the standard itself: whether it is enforced by a linter or a template, what it means for scripts that are sourced rather than executed, and when the honest answer is that a script needing this much defensive scaffolding should be a program in a real language instead.
## The default bash contract Bash's defaults were designed for an interactive session, where you see each command's output and decide what to do next. A script inherits those defaults, and they are actively hostile to unattended execution: - A command that fails is not fatal. The shell prints whatever the command printed and moves to the next line. - Expanding a variable that was never set yields the empty string, silently. - A pipeline's exit status is the status of its **last** command only. Each default turns a loud failure into a quiet wrong result. The canonical horror is a build script that fails to produce an artifact and then happily uploads the stale one, reporting success. ## What each option changes **`set -e`, long form `set -o errexit`.** The shell exits as soon as a *simple command*, a pipeline, or a compound command returns a non-zero status, and the script's own exit status becomes that failing status. It is the single biggest behavioural change of the three, and also the most misunderstood, because bash deliberately suspends it wherever a non-zero status is being *used as data* — the condition of `if`/`while`/`until`, any command in a `&&` or `||` list except the final one, and a command whose status is inverted with `!`. **`set -u`, long form `set -o nounset`.** Referencing an unset variable or an unset positional parameter is an error; in a non-interactive shell that means the script prints something like `myvar: unbound variable` and exits. This catches misspelt names and, more importantly, environment variables the caller forgot to export. It is a runtime check, not a parse-time one: the line has to actually execute before it fires. Where an empty value is legitimate, you opt out per-expansion with a default, `"${MYVAR:-}"`, rather than turning the option off. ```bash set -u echo "${GREETING:-hello}" # fine: a default is supplied echo "$GREETING" # error: GREETING is unbound ``` **`set -o pipefail`.** Without it, `curl -fsS "$url" | tar -xz` reports only `tar`'s status, so a failed download that produces no bytes looks like success when `tar` happens to exit zero. With it, the pipeline takes the rightmost non-zero status among its stages. There is no single-letter form; it must be written with `-o`. ## Why one line rather than three habits The options are cheap to add and, unlike a code-review convention, they apply to every line written afterwards, including lines added later by someone who has not read the review guidelines. Putting them on the first executable line (immediately after the shebang, before any work) makes the whole file obey them. They are bash options, not POSIX ones — `pipefail` in particular is not in POSIX and older `/bin/sh` implementations reject it — so the script must genuinely be running under bash for the line to be meaningful. Some teams add a fourth element, `IFS=$'\n\t'`, to narrow the field separator used for word splitting; that is a separate concern from error handling and has its own tradeoffs. ## What the line does not buy This is where interviews go, so be ready for it: - `set -e` does not fire in a tested position. `if risky; then` and `risky && next` both run `risky` with errexit suspended, and if `risky` is a shell function, the suspension applies to the *entire function body*. - Assignment through a declaration builtin hides status: `local out=$(failing_cmd)` exits 0 because the status reported is `local`'s, not the command substitution's. - Commands that report failure as *output* rather than status are untouched. A tool that prints an error and exits 0 still looks successful. - `set -u` says nothing about a variable that is set but empty or nonsense. - Nothing here produces a diagnostic. Under `set -e` the script dies silently at the failing line; an operator gets an exit code and no message. That is why hardened scripts add an explicit `|| die "..."` habit and often an ERR trap on top. ## Turning it off deliberately `set +e`, `set +u`, `set +o pipefail` disable each option again, and `$-` shows the currently active single-letter flags. Scoping a relaxation as narrowly as possible — ideally `cmd || true` on the one line that is allowed to fail — is preferred to a wide `set +e` block, because a wide block silently un-protects everything inside it.
- How do you let one specific command fail without aborting a script that runs under `set -e`?Append `|| true` to that single command, which makes the list succeed regardless. Alternatives are wrapping it in an `if`, or capturing the status explicitly with `cmd || status=$?`. Prefer any of those to a broad `set +e` … `set -e` block, because the block un-protects every line inside it, including lines someone adds later.
- Under `set -u`, is `"$@"` an error in a script that was called with no arguments?Not in modern bash. Bash 4.4 and later treat `"$@"` and `"$*"` with no positional parameters as expanding to nothing rather than as an unbound reference. Older bash — including the 3.2 that ships on macOS — errors out, which is why defensive scripts targeting it write `"${@:-}"` or guard with `(( $# ))`.
- What is the difference between `${VAR:-default}` and `${VAR-default}` when `set -u` is active?`${VAR:-default}` substitutes the default when VAR is unset *or* set to the empty string; `${VAR-default}` substitutes only when VAR is unset, and passes an explicitly empty value through. Both satisfy `set -u`, because supplying a default makes the expansion legal. Use the colon form when empty and missing should be treated the same.
saying these in an interview costs you the question
- Claims set -e catches every possible failure
- Thinks set -u is a parse-time check for typos
- Believes pipefail is on by default in bash
- Says strict mode makes a script POSIX-portable
- Thinks set -e prints a diagnostic when it aborts