Writing a Bash Script That Won't Bite You
Introduction¶
Most operational bash scripts are one unquoted variable away from disaster. A short set of habits — a strict-mode header, quoting everything, a cleanup trap — removes almost all of that risk without turning bash into a different language.
The header¶
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
-e— exit on any command that fails (non-zero). No more "the script kept going aftercdfailed andrm -rfran in the wrong directory."-u— error on an unset variable. Catches typos andrm -rf "$DIR/"when$DIRis empty (it becomesrm -rf /).-o pipefail— a pipeline fails if any stage fails, not just the last. Without it,curl badurl | jq .succeeds becausejqsucceeded.IFS=$'\n\t'— word-split only on newline and tab, not spaces. Filenames with spaces stop breaking loops.
Working with -e's edge cases¶
-e doesn't trigger for commands in if, &&, ||, or ! — that's by
design. When you want to check a command that might fail:
if ! systemctl is-active --quiet nginx; then
echo "nginx is down"
fi
# Capture a possibly-failing command without aborting
output=$(some_command) || output="default"
Quote everything¶
# Wrong — breaks on spaces, globs, empty values
cp $src $dst
for f in $(ls *.log); do rm $f; done
# Right
cp -- "$src" "$dst"
for f in *.log; do # let the shell glob; no ls
[ -e "$f" ] || continue # handle "no matches"
rm -- "$f"
done
"$@" (quoted) preserves each argument; $* and unquoted $@ don't. Use --
before filename arguments so a file named -rf isn't parsed as a flag.
Safe temp files and cleanup¶
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
# ...use "$tmpdir"...
# cleanup runs on normal exit, error (thanks to -e), or Ctrl-C
trap ... EXIT is the single most useful pattern — it guarantees cleanup no
matter how the script ends. Add specific signals if you need different handling:
trap 'echo interrupted; exit 130' INT.
Arguments and usage¶
usage() { echo "usage: $0 [-v] -e ENV SERVICE" >&2; exit 2; }
verbose=0 env=""
while getopts ":ve:h" opt; do
case "$opt" in
v) verbose=1 ;;
e) env="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
[ -n "$env" ] || usage
service="${1:?service name required}"
The classic footguns¶
rm -rf "$dir/"with$dirunset →-ucatches it; alsorm -rf "${dir:?}/"aborts if empty.cd "$dir" && rm -rf *— ifcdfails and there's no-e, yourmin the current directory.-efixes it;cd "$dir" || exit 1is explicit.- Parsing
lsoutput — never. Glob, orfind ... -print0 | xargs -0. [ $x == "y" ]when$xis empty →[ == "y" ]→ syntax error. Quote:[ "$x" == "y" ], or use[[ ]].echofor anything with backslashes or leading-→ useprintf '%s\n'.- Comparing numbers with
</>in[ ](that's string comparison) — use(( x > 5 ))or[ "$x" -gt 5 ].
Lint it¶
shellcheck myscript.sh
ShellCheck catches every quoting and word-splitting bug above and explains
each. Run it in CI. bash -n myscript.sh is a syntax-only check.
Verification¶
bash -n script.sh # parse without running
bash -x script.sh # trace every expanded command
shellcheck script.sh
- Script "works" but does nothing on some inputs — an unquoted variable
word-split, or a glob that matched nothing and was passed literally.
-xshows the actual expanded command. -eset but the script continues past a failure — the failing command is in a pipeline (needpipefail), or in anif/||context (by design), or it's a function whose last command "succeeded."- Cleanup didn't run — the script was
kill -9'd (uncatchable), or thetrapwas set after the failure point. Set traps early.
Related tools and reading¶
- On-site: Regex Tester, Linux commands reference.
- Related posts: Bash parameter expansion cheatsheet, journalctl recipes.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Linux & Sysadmin
resolv.conf and systemd-resolved
On modern distros /etc/resolv.conf points at a stub resolver managed by systemd-resolved. Editing it directly usually does nothing — here's the real model.
October 24, 2026Tuning Open File Limits on Linux
Four different limits control how many files a process can open. 'Too many open files' means you're hitting one specific one — here's how to find and raise it.
October 24, 2026ss and conntrack for Connection Debugging
ss shows you every socket and its send/receive queues; conntrack shows what the firewall is tracking. Together they explain most 'connection refused' and 'no buffer space' problems.
October 24, 2026