Safer scripts: set -euo pipefail

Bash · Practical · 12 min

Without these options a script keeps going after a failure and can do real damage with empty variables.

By default bash ignores failures and keeps running. Three options, usually written together at the top of a script, change that:

  • set -e (errexit): exit when a command fails.
  • set -u (nounset): treat expanding an unset variable as an error, which catches typos like rm -rf "$buidl_dir/".
  • set -o pipefail: a pipeline fails if any stage fails, not just the last.

set -e has deliberate exceptions you must know. It does not trigger for commands in an if/while condition, for any command in a &&/|| list except the last, or for commands negated with !. And the status of a command substitution is lost when it is used in local x=$(cmd) or echo "$(cmd)"; declare first (local x) and assign on its own line.

Commands that legitimately return non-zero need handling: grep returns 1 for no match, diff for differences. Use || true, or test them in an if.

With -u, optional parameters need defaults: "${1:-}", "${DEBUG:-0}". Some people also set IFS=$'\n\t' so unquoted expansions only split on newlines and tabs. Strict mode is a safety net, not a substitute for checking the errors you can anticipate and printing helpful messages.

Example

cd "$(mktemp -d)"
cat > strict.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
name=${1:-world}              # -u: optional args need defaults
echo "hello $name"
count=$(grep -c foo /dev/null || true)   # grep exits 1 on no match
echo "count=$count"
if ! mkdir /proc/x 2>/dev/null; then     # failures inside if are fine
  echo "mkdir failed, handled"
fi
echo "$undefined_var"         # -u: the script dies here
echo "never printed"
EOF
bash strict.sh; echo "exit status: $?"
# hello world / count=0 / mkdir failed, handled
# strict.sh: line 10: undefined_var: unbound variable
# exit status: 1

Remember

  • set -euo pipefail: stop on errors, unset variables and failed pipe stages
  • -e is suspended in conditions, && / || lists and after !
  • Give optional variables defaults with ${var:-}

Watch out for

  • local out=$(cmd) hides cmd's failure from set -e because local itself succeeds.

Check yourself

  1. Under `set -e`, does `if grep -q x file; then ...` abort the script when x is not found?
  2. What does `set -u` catch?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin