Explain 'set -euo pipefail' and why it should be at the top of every production Bash script. What are the gotchas?
Quick Answer
set -e exits on error, -u exits on undefined variables, -o pipefail catches errors in piped commands. Gotchas: set -e doesn't catch errors in conditionals, subshells, or command substitutions.
Detailed Answer
Each Flag
set -e(errexit): Exit immediately if any command returns non-zero. Without this, scripts silently continue after errors.set -u(nounset): Treat unset variables as errors. Prevents typos likerm -rf $UNSET_VAR/from becomingrm -rf /.set -o pipefail: A pipeline returns the exit code of the last failed command, not the last command. Without this,curl fail | grep okreturns 0 (grep's exit code) even if curl failed.
Gotchas with set -e
1. Commands in if conditions are exempt: if command_that_fails; then doesn't exit 2. Commands before || are exempt: command_that_fails || true is fine 3. Command substitutions: result=$(failing_command) exits, but local result=$(failing_command) does NOT (local's exit code masks it) 4. Subshells: (failing_command) exits the subshell but the parent continues 5. Arithmetic: let 'x=0' returns non-zero exit code (gotcha!)
Best Practice
Combine with trap for cleanup and meaningful error messages.
Code Example
#!/usr/bin/env bash
set -euo pipefail
# Trap for cleanup on exit
trap 'echo "Error on line $LINENO. Exit code: $?" >&2; cleanup' ERR
trap cleanup EXIT
cleanup() {
rm -f "$TMPFILE" 2>/dev/null
}
TMPFILE=$(mktemp)
# Gotcha: local masks exit code!
my_func() {
local result=$(false) # Does NOT trigger set -e!
result=$(false) # This DOES trigger set -e
}
# Gotcha: arithmetic
(( count = 0 )) # Exit code 1! Use: count=0 instead
# Safe default variable
DEPLOY_ENV=${DEPLOY_ENV:-staging}
# Pipefail in action
curl -sf https://api.example.com/health | jq '.status'
# Without pipefail: returns jq's exit code even if curl fails
# With pipefail: returns curl's non-zero exit codeInterview Tip
The 'local' gotcha is what separates seniors from juniors. Most people know set -e but don't know that 'local var=$(cmd)' masks the exit code. Always split: local var; var=$(cmd).