The defensive header
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'Most professional bash scripts start with these two lines. Without them, errors get swallowed silently and bugs go unnoticed.
What each flag does
- -e (errexit) — exit immediately when any command returns non-zero (with carve-outs for tests and pipelines).
- -u (nounset) — exit when an undefined variable is referenced. Catches typos like
$PROOJECT. - -o pipefail — pipeline exit code is the first non-zero stage's, not just the last command's.
- IFS=$'\n\t' — restrict word splitting to newlines and tabs (not spaces). Helps with filenames and multi-word values.
Carve-outs you'll need
set -e doesn't fire if the command is part of an if, a && chain, or piped into |. Use that to your advantage:
# OK — caller checks the exit code
if grep -q ERROR build.log; then ...
# Tolerate failure with || true
rm -rf .cache || trueDefault with -u
With -u, referencing an unset variable explodes. The ${var:-} form gives an empty default, ${var:-fallback} gives a real one. Always defensive-default user-supplied env vars: "${DEBUG:-0}".
Custom error reporting
err() { echo "$0:${LINENO}: $*" >&2; exit 1; }
[[ -d "$dir" ]] || err "missing $dir"A small err helper makes script failures actionable. Pair with trap (next lesson) for cleanup on the way out.