Lesson 1 of 9 · 15m
set -euo pipefail: the First Five Lines
What errexit, nounset, and pipefail actually do, where set -e quietly does nothing, and the local bug.
You inherit a deploy script that prints "done" no matter what happened inside it. The last outage it caused traced back to a grep that failed halfway through — the script sailed on, applied a partial config, and left the box in a state nobody could reproduce. This lesson is the five minutes of setup that prevents that entire class of failure.
Strict mode: the only sane default
Every Bash script you write should start like this:
#!/usr/bin/env bash
set -euo pipefailThree switches, three different problems:
set -e(errexit): the shell exits when a command returns non-zero. This is the single biggest behavioral change — a failing command no longer gets silently stepped over.set -u(nounset): expanding an unset variable is an error, not an empty string.rm -rf "$BULID_DIR"stops dead because you typo'dBUILD_DIR— instead of evaluating torm -rf ""or worse.set -o pipefail: a pipeline returns the rightmost non-zero exit, not just the last command's. Without it,grep -q token /etc/hosts | head -1reports success becauseheadsucceeded, even whengrepfailed. With it, the pipeline propagatesgrep's status.
pipefail is not POSIX. It works in Bash (3.x+), zsh, and ksh — but not in dash, which is what /bin/sh points to on Debian and Ubuntu. If your script relies on it, say #!/usr/bin/env bash and mean it. Never assume sh is Bash.
Where set -e quietly does nothing
Errexit is disabled inside if, while, and until conditions, on the left side of &&/||, and under !. That's by design — the condition needs to see the exit code:
if grep -q 'listen 443' /etc/nginx/nginx.conf; then
echo "tls site configured"
fiThe trap that ships to production: local. local is a builtin, and it masks the command's failure:
f() {
local out=$(cat /nonexistent) # set -e does NOT catch this
echo "$out"
}local exits 0, so errexit stays quiet. Write it on two lines:
local out
out=$(cat /nonexistent) # now set -e fires correctlyAlso know that set -e inside command substitution ($(...)) behaves subtly — a failing command inside $( ) does not kill the script; it just sets $?. Check it when it matters.
Debugging with tracing
set -x traces every command before it runs. Make the trace useful:
export PS4='+ ${BASH_SOURCE}:${LINENO}: 'Now every trace line tells you which file and line it came from — invaluable inside functions.
Try this
Run this twice — once with set -o pipefail, once without:
#!/usr/bin/env bash
set -euo pipefail
grep -q 'zzz' /etc/hosts | head -1
echo "made it"Without pipefail, you reach "made it". With it, the script exits at the pipeline. That's the difference between a silent skip and a caught failure.
Field notes
grepreturning 1 on "no matches" is a feature — usegrep -q ... || truewhen "not found" is a legitimate outcome, not a failure.set -uwill bite you on${1:?usage: $0 <arg>}-style checks — that's intentional. Provide defaults with${var:-default}.- Never
evalscript input. If you think you need it, you want an array (covered in lesson 3).