Write robust scripts
Make scripts fail safely with strict mode, clean up with traps, parse options with getopts, and debug.
- Explain what set -euo pipefail does, and its gotchas
- Report errors to stderr, guard dangerous expansions and clean up with trap
- Parse options with getopts and debug with bash -x and ShellCheck
By default, Bash carries on after a command fails. That’s friendly at the prompt and dangerous in a script: if cd /backups fails, the rm -rf * on the next line runs wherever you happen to be. Many scripts start with strict mode:
#!/usr/bin/env bash
set -euo pipefail| Option | Effect |
|---|---|
-e (errexit) | exit as soon as a command fails |
-u (nounset) | treat an unset variable as an error instead of an empty string |
-o pipefail | a pipeline fails if any stage fails, not just the last |
1grep reactor /no/such/file 2>/dev/null | sort
2echo "without pipefail, the pipeline's status is $? (sort's)"
3set -o pipefail
4grep reactor /no/such/file 2>/dev/null | sort
5echo "with pipefail, it's $? (grep's)"without pipefail, the pipeline's status is 0 (sort's) with pipefail, it's 2 (grep's)
Defensive habits
- Quote every expansion:
"$file","${files[@]}","$(cmd)". - Guard dangerous paths:
rm -rf "${build_dir:?}/"*stops the script with an error ifbuild_diris empty, instead of deleting from/. - End options with
--:rm -- "$file"stays safe even if a file is named-rf. - Errors go to stderr, with a non-zero exit: a small
diefunction keeps it tidy. - Clean up with
trap: create temporary files withmktempand remove them in anEXITtrap. - Check inputs early and print a usage message.
1set -euo pipefail
2die() { echo "error: $*" >&2; exit 1; }
3work=$(mktemp -d)
4trap 'rm -rf "$work"; echo "cleaned up"' EXIT
5
6target="${1:-}"
7[[ -n $target ]] || target="kestrel" # a default instead of dying
8printf '%s\n' alpha beta > "$work/list.txt"
9count=$(wc -l < "$work/list.txt")
10echo "processed $count items for $target"
11[[ -d $work ]] && echo "temp dir exists until exit"processed 2 items for kestrel temp dir exists until exit cleaned up
Try it
Review this backup script
A crewmate wrote this nightly backup script. Click every line that could cause trouble, then check what you missed.
Click every part that looks suspicious. There are 6.
Options with getopts
Real tools take options: report -v -n 5 station.log. The getopts built-in parses short options for you. The option string lists the letters; a colon after a letter means it takes a value (in OPTARG); a leading colon lets you handle errors yourself:
1set -- -v -n 2 Kestrel # as if run: ./hail.sh -v -n 2 Kestrel
2usage() { echo "usage: hail [-v] [-n count] name" >&2; exit 2; }
3verbose=false count=1
4while getopts ":vn:h" opt; do
5 case $opt in
6 v) verbose=true ;;
7 n) count=$OPTARG ;;
8 h) usage ;;
9 :) echo "-$OPTARG needs a value" >&2; usage ;;
10 ?) echo "unknown option -$OPTARG" >&2; usage ;;
11 esac
12done
13shift $(( OPTIND - 1 )) # drop the options; "$@" is now the rest
14name=${1:?missing name}
15$verbose && echo "count=$count name=$name"
16for (( i = 0; i < count; i++ )); do echo "Hailing $name"; donecount=2 name=Kestrel Hailing Kestrel Hailing Kestrel
Debugging
bash -n script.shchecks the syntax without running anything.bash -x script.sh, orset -xinside it, prints every command after expansion, prefixed with+- you see exactly what ran.set +xturns it off again.- ShellCheck is a linter that catches unquoted variables, useless
cats,$(ls)loops and hundreds of other mistakes. Most editors can run it as you type. Use it on every script.
1deck="deck 4"
2set -x
3ls $deck 2>/dev/null
4set +x
5echo "the trace (on stderr) shows ls received two arguments"the trace (on stderr) shows ls received two arguments
Key takeaways
set -euo pipefailstops on failures, unset variables and broken pipelines - mind its gotchas.Quote everything, guard with
${var:?}, use--, and send errors to stderr with a non-zero exit.trap ... EXITcleans up;getoptsparses options;shift $((OPTIND - 1))leaves the arguments.Debug with
bash -xand lint every script with ShellCheck.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: write Bash scripts
Write a script in the editor and run it for real against sample input. Each run gets a fresh Linux sandbox with Bash 5.2 and the GNU tools on Wandbox, a free public service - so experiment freely, even with rm. Your script and test input are sent there.
Parse the options
The starter turns the line on stdin into the script’s arguments. Use getopts to support:
-n COUNT- how many times to greet (default 1)-u- upper-case the name-v- first printcount=N name=NAME(before any upper-casing)
then a name argument. Print Hello, NAME! COUNT times. For example, -u -n 2 Ada prints Hello, ADA! twice.
- -n 2 Kestrel
- -u -n 2 Ada
- -v Grace
- Just a name
Your script runs with Bash 5.2 and GNU tools on Wandbox, a free public service, in a fresh sandbox each time. Your script and test input are sent to that service.
Clean up after yourself
The starter makes a temporary directory and processes some samples, but never cleans up. Add an EXIT trap that calls cleanup, and write cleanup so that it prints cleaning up N files (the number of files in the temp directory), deletes the directory, and then prints removed: yes if it’s gone.
1working...
2result: 3 samples
3cleaning up 3 files
4removed: yes- Cleans up
Your script runs with Bash 5.2 and GNU tools on Wandbox, a free public service, in a fresh sandbox each time. Your script and test input are sent to that service.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…