Shell scripting, one move per page
The twenty-two moves past one-liners: quote everything, fail fast, clean up on every exit, and never lose a Saturday to an unquoted variable.
You know the commands. These are the moves.
Shell scripting, one move per page
The twenty-two moves past one-liners: quote everything, fail fast, clean up on every exit, and never lose a Saturday to an unquoted variable.
Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).
Shell behaviour verified against the GNU Bash manual: gnu.org/software/bash/manual, and the man-pages project: man7.org/linux/man-pages.
This book is the companion to 'The Linux terminal, one command per page' and stands alone: it assumes the everyday commands and teaches the moves.
Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.
© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.
Edition 1.0 · stevehodgkiss.net
Contents
Quoting: double quotes preserve the value of variables while stopping word splitting and globbing; unquoted expansions are split and globbed by the shell. From the GNU Bash manual, Shell Expansions.
Quoting "$var"
Let's say a script works on every file you test it with, then dies on my file.txt. The space did it.
Double quotes preserve the value while stopping word splitting and globbing. Unquoted, the shell splits the expansion into separate words and globs any wildcards, so one argument quietly becomes two.
Quote every expansion. The variable, not the filename you tested with.
Run: f='my file.txt'; touch "$f"; then try touch $f without quotes and watch two files appear.
Strict mode
The two set lines and one option that stop a script at the scene of the failure instead of three steps later.
- 01set -e and -u
- 02pipefail
set -e exits the shell immediately if a command exits non-zero, with exceptions for commands in conditions; set -u treats unset variables as errors. From the GNU Bash manual, The Set Builtin.
set -e and -u
Step 3 of the deploy fails. Without strict mode the script cheerfully runs steps 4, 5 and 6 on the broken state.
set -e exits the moment a command returns non-zero. set -u makes an unset variable an error instead of an empty string. Commands inside if and && conditions don't trigger -e, so a failure you only test for still needs handling.
The script dies at the scene of the failure, not three steps later.
Fail at step 3, not after step 6 has made it worse.
Write a three-line script starting with set -eu, let line 2 be false, and watch line 3 never run.
set -o pipefail makes the pipeline's return status the last command that exited non-zero, instead of the default which is the last command's status. From the GNU Bash manual, Pipelines.
pipefail
grep throws up a million lines, so you pipe through head -10. head exits zero. grep's broken pipe error never reaches your if.
By default a pipeline's status is the last command's status. set -o pipefail makes it the rightmost command that failed. Without it, a failed first stage hides behind a successful last one.
Combined with -e, a pipeline failure stops the script instead of blessing it.
The pipeline answers for every stage, not just the last mouth.
set -o pipefail; then run false | true; echo $? and get 1. Run it again without pipefail and get 0.
trap 'cleanup' EXIT registers a command to run when the shell exits, whether by reaching the end, an error, or a signal like INT. From the GNU Bash manual, Bourne Shell Builtins, trap.
trap on EXIT
The script makes a lockfile and a temp directory. Halfway through, it errors out. Both stay behind.
trap 'rm -f /tmp/x.lock' EXIT runs when the shell exits: normally, on error, on Ctrl-C. Cleaning up at the bottom of the script only covers the happy path, and the happy path is the one that doesn't litter.
One line at the top and every exit path sweeps up.
Write the cleanup once, at the top, for every ending.
Script: trap 'echo cleaning up' EXIT, then a line that runs false under set -e. Watch the trap fire anyway.
stdin, stdout and stderr are file descriptors 0, 1 and 2; 2> redirects errors, 2>&1 sends errors to wherever stdout points, and &> file sends both. From the GNU Bash manual, Redirections.
2>&1, and friends
You redirect the output to a log and the log is empty, while errors spray the terminal all night.
Errors travel on channel 2, and 2>err.log sends just them. 2>&1 points errors wherever stdout already goes, and order matters: cmd >log 2>&1. cmd 2>&1 >log leaves errors on the terminal, because the copy happened first.
Log what you meant to log, on the channel it actually travels.
Redirect is a pointer. Order is the whole sentence.
Run ls . missing 2>&1 >out.txt and then ls . missing >out.txt 2>&1. Compare what hits the terminal.