OnCallReady

Chapter 6 Bash Scripting

The header and what it does not catch, exit status and subshells, quoting, reading input, [[ ]], parameter expansion, arrays, functions, traps, redirection, heredocs, getopts and shellcheck.

In plain words

Imagine writing instructions for a helper who does exactly what the note says, word for word, and never asks questions. If you write "put the old report in the bin" and forget the quotes, the helper hears "put the old" and "report in the bin" as two jobs. If one step fails, the helper happily carries on with the next unless you told them to stop. And if you forget to name something, they use nothing at all, and "delete everything in nothing slash" becomes "delete everything".

That helper is bash. A script is your note. This chapter is how to write notes that stop on the first failure (set -euo pipefail), keep names together (quoting), check things properly ([[ ]]), clean up after themselves (trap), and how to proofread them with shellcheck and bash -x before you hand them over.

Why it matters on call

Bash is the glue of platform work: backup and cleanup scripts, deploy and release scripts, the helper scripts systemd services and timers run, the setup script a new server runs on its first boot, and runbook one-liners (a runbook is the written step-by-step for handling a known problem). Most of it is written quickly and breaks in the edge cases: a filename with a space, an empty variable, a failed command in a pipe, a cd that did not happen. Those bugs ship broken artifacts, delete the wrong directory or report success on failure.

This chapter turns scripts from "worked when I tried it" into something you can trust in automation and defend in review. It also covers the classic interview topics: what set -e misses, why local x=$(cmd) hides errors, how to clean up on exit, "$@" versus $*. And the final incident, ship.sh, is exactly the kind of script you will inherit in a platform team.

Lessons

  1. The header every script gets
  2. What set -e does NOT catch
  3. Exit status, subshells and case
  4. Quoting
  5. Reading input: while read, IFS and mapfile
  6. [[ ]] versus [ ]
  7. Parameter expansion and arrays
  8. Functions, traps and mktemp
  9. Redirection and file descriptors
  10. Here-documents
  11. getopts and a usage function
  12. shellcheck, bash -n and bash -x

15 hands-on labs (missions, incidents and drills) run in the terminal: Open this chapter in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.

Questions people ask

Should I write scripts in bash or in Python?

Bash is best for orchestrating commands: short glue that runs tools, moves files and checks exit codes, especially where only a shell exists (minimal servers, rescue shells, recovery). Once a script needs real data structures, heavy data parsing, error handling with retries, or grows past a couple of hundred lines, Python or Go is usually clearer and easier to test. Many teams use both, with shellcheck on everything in bash.

Is sh the same as bash?

No. sh is the POSIX shell language; on Ubuntu /bin/sh is dash, a small fast POSIX shell without bash features like arrays, [[ ]], $'...', <<<, {1..5} or pipefail (newer dash versions added pipefail). A script with #!/bin/sh that uses bash syntax fails on Ubuntu and Debian but works where sh is bash. Use #!/usr/bin/env bash when you use bash features.

Is set -euo pipefail enough to make a script safe?

It is a strong default, not a guarantee. -e does not fire in conditions, before &&/||, in non-final pipeline stages without pipefail, or for local x=$(cmd). -u does not catch empty-but-set variables. Combine it with quoting, explicit checks on important commands, ${var:?} for required values, traps for cleanup, and shellcheck. Some experienced people dislike set -e precisely because of the gaps.

Why does quoting matter so much?

Because an unquoted expansion is split into words on spaces, tabs and newlines and then each word is glob-expanded. A path with a space becomes two arguments, a value with * becomes a list of files, and an empty value disappears entirely, shifting other arguments. Those turn into wrong files deleted, commands getting the wrong arguments and "too many arguments" errors. Quoting every expansion avoids the whole class.

What tools should every bash script go through?

shellcheck for static analysis, run every time a script changes; it finds unquoted variables, masked exit codes, missing || exit after cd and dozens more real bugs. bash -n for a syntax check without running anything. bash -x or set -x to trace what actually executed. shfmt for consistent formatting helps review. For larger scripts, a test framework like bats.