Safer Scripts with set -e, -u, -o pipefail, and -x
Four Bash options that instantly make your scripts safer: exit on error, error on unset variables, propagate pipe failures, and trace execution.
By default, Bash keeps running even after a command fails. It also ignores unset variables (treating them as empty) and ignores failures in the middle of pipes. Adding four options to the top of every script fixes all three problems.
The Safe Script Header
#!/usr/bin/env bash
set -euo pipefail
# ↑ This single line dramatically reduces silent failures in scriptsset -e — Exit on Error
#!/usr/bin/env bash
set -e # exit immediately if any command exits with a non-zero code
echo "Before"
ls /nonexistent # ← This fails; script STOPS here
echo "This line never runs"Nuances of set -e
set -e does NOT exit on all failures. It's skipped when:
- The command is in an
if,while, oruntilcondition - The command is followed by
|| ...or&& ... - The command is in a pipeline (the last command's exit code is checked)
For example: if grep -q pattern file; then — grep failure here won't exit the script.
set -u — Error on Unset Variables
#!/usr/bin/env bash
set -u # treat unset variables as an error
name="Alice"
echo "$name" # Alice
echo "$undefined" # ← Error: unbound variable — script exits
# Use ${var:-default} to safely access potentially-unset variables
echo "${undefined:-fallback}" # fallback — no error with set -uset -o pipefail — Propagate Pipe Failures
# Without pipefail:
cat nonexistent_file | sort | head -5
echo $? # 0 — only head's exit code matters!
# With pipefail:
set -o pipefail
cat nonexistent_file | sort | head -5
echo $? # non-zero — cat's failure propagates
# Common gotcha: grep returning 1 (no match) fails with pipefail
# Fix: use grep || true to allow no-match
grep "pattern" file | head -5 || true # won't fail script if no matchset -x — Debug Trace
#!/usr/bin/env bash
set -x # print each command before executing it (prefixed with +)
name="Alice"
echo "Hello, $name"
# Output:
# + name=Alice
# + echo 'Hello, Alice'
# Hello, Alice
# Turn off tracing for part of the script
set +x
# quiet section
set -x
# tracing resumesCombining Options and ERR Traps
#!/usr/bin/env bash
set -euo pipefail
# Custom error handler
function on_error {
local line=$1
echo "Script failed at line $line" >&2
}
trap 'on_error $LINENO' ERR
# Temporarily disable set -e for expected failures
set +e
grep "maybe" file.txt
found=$?
set -e
if [[ $found -eq 0 ]]; then
echo "Pattern found"
else
echo "Pattern not found (that's OK)"
fiWith `set -o pipefail`, which command's exit code determines if `cmd1 | cmd2 | cmd3` fails?
Take this risky script and make it safe:
#!/usr/bin/env bash
# RISKY version
dir=$HOME/important
cd $dir
rm -rf *
echo "Done: $(pwd)"
Add set -euo pipefail, handle the case where $dir doesn't exist gracefully, and add a safety check before the rm.