Guide

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

Bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

target="${1:?usage: safe.sh <dir>}"
echo "cleaning ${target}"

What each line does:

  • set -e exits on the first command that fails.
  • set -u treats an unset variable as an error.
  • set -o pipefail makes 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"' EXIT runs even when the script fails.
  • Run shellcheck in CI. It catches quoting bugs and most set -e traps.
  • 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 -e alone. It is ignored inside if conditions 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 -u off because a variable is "optional". Use ${VAR:-default} to make the optional case explicit.

Check yourself

  1. Why does set -e miss a failure in false && echo ok? Commands joined by && or || are exempt from set -e, so the failure does not stop the script.
  2. What does curl -f change? 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 pipefail and a fixed IFS.
  • Quote every variable, and use ${VAR:?message} for required inputs.
  • Use trap for cleanup and curl -f for 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 Semester