Linux and networking
Safe bash scripts
Bash fails quietly by default. A few lines at the top of every script turn unset variables and hidden failures into loud errors.
Bash is forgiving in ways that hurt in production. An unset variable quietly becomes an empty string. A failing command in the middle of a script is ignored. A failure on the left side of a pipe disappears. The fix is a short preamble that makes all of these fail loudly, and a few habits that keep the rest of the script honest.
The big idea
Think of a script as a checklist that does not stop when an item fails. The preamble tells bash to stop at the first failed item, to treat a missing variable as a failure, and to report a failed pipeline stage as a failed pipeline.
Walking through it
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
target="${1:?usage: safe.sh <dir>}"
echo "cleaning ${target}"What each line does:
set -eexits on the first command that fails.set -utreats an unset variable as an error.set -o pipefailmakes a pipeline fail if any stage fails, not only the last one.IFS=$'\n\t'stops words from splitting on spaces, so a filename with a space stays one argument.${1:?usage: ...}stops with a message if the first argument is missing.
Run with no argument, the script stops before it prints anything, and echo $? prints 1. The error message names the line and the missing argument.
The reason this matters: a script with rm -rf "$DIR/"* and an unset DIR expands to rm -rf /*. Steam's Linux client shipped a bug of exactly this shape in 2015 that deleted users' home directories. Failing on unset variables blocks that path.
Rules that prevent most bash bugs
- Quote every expansion. Write
"$var"and"$@". Unquoted variables split on spaces and get globbed. - Use
curl -f. Without it, a 404 page is saved as your tarball and the script reports success. - Clean up with
trap.trap 'rm -rf "$tmp"' EXITruns even when the script fails. - Run shellcheck in CI. It catches quoting bugs and most
set -etraps. - Know when to stop. Past about 100 lines, or once the script needs data structures or parses complex data, rewrite it in Python or Go.
Common mistakes
- Relying on
set -ealone. It is ignored insideifconditions and in commands joined with&&or||. Check critical steps yourself. - Forgetting cleanup on failure. Without
trap, temporary files pile up on every failed run. - Leaving
set -uoff because a variable is "optional". Use${VAR:-default}to make the optional case explicit.
Check yourself
- Why does
set -emiss a failure infalse && echo ok? Commands joined by&&or||are exempt fromset -e, so the failure does not stop the script. - What does
curl -fchange? An HTTP error such as 404 becomes a non-zero exit code, so the script does not save the error page as a file.
Key takeaways
- Start every script with
set -euo pipefailand a fixedIFS. - Quote every variable, and use
${VAR:?message}for required inputs. - Use
trapfor cleanup andcurl -ffor downloads, and run shellcheck in CI.
Further learning
Stopping a script or service cleanly is the next idea, in graceful shutdown.
ShellCheckShellCheck finds bugs in your shell scriptsshellcheck.net
The Missing Semester of Your CS Education (MIT)Master powerful tools that will make you a more productive computer scientist and programmer.Missing SemesterRelated
Linux for engineers
The parts of Linux you touch in production, from shell pipelines and systemd services to permissions, disks, network debugging commands and safe remote access.
Shell pipes and streams
How programs talk through stdin, stdout and stderr, and how pipes chain small tools into one answer to a production question.
Graceful shutdown
How orchestrators stop a process with SIGTERM, then SIGKILL after a grace period, and how a service drains in-flight work inside that window.