← All 45 books Shell scripting, one move per page Get the full edition · £10
One move per page

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.


Steve Hodgkiss 5 moves

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

Contents


Part 1 · The words
Quoting "$var"4
Part 2 · Strict mode5
set -e and -u6
pipefail7
Part 3 · The safety net
trap on EXIT8
Part 4 · Tests and plumbing
2>&1, and friends9

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.

Shell · No. 01
Foundations

Quoting "$var"

The shell rewrites unquoted words before the command ever runs

THE VARIABLE HOLDS ONE NAME. THE SHELL MAY DELIVER TWO.f=my file.txtone variable, one valuerm $fshell splits on the space:rm my file.txttwo arguments, one is wrongrm "$f"quotes keep it one word:rm "my file.txt"one argument, exactly rightthe lessonv=a b ctouch $v # 3 filestouch "$v" # 1 filequote everyexpansion

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.

TRY IT THIS WEEK

Run: f='my file.txt'; touch "$f"; then try touch $f without quotes and watch two files appear.

Part 2 of 4
Fail fast, on purpose
2

Strict mode

The two set lines and one option that stop a script at the scene of the failure instead of three steps later.


In this part
  1. 01set -e and -u
  2. 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.

Shell · No. 02
Strict mode

set -e and -u

Stop the script the moment something fails

STRICT MODE: THE SCRIPT DIES AT THE SCENE, NOT LATER.deploy.sh#!/usr/bin/env bashset -eubuilduploadnotifyupload fails, exit 1stops herenotify never runsno half-deploysalso -u:unset varis an error,not empty

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.

TRY IT THIS WEEK

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.

Shell · No. 03
Strict mode

pipefail

A pipeline is only as honest as its leakiest stage

THE PIPELINE REPORTS ITS LAST STAGE. UNLESS YOU SAY OTHERWISE.grepexit 1headexit 0default result:0a liewith pipefail:1the truthset itset -opipefailfalse |true

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.

TRY IT THIS WEEK

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.

Shell · No. 04
Safety net

trap on EXIT

The cleanup that runs no matter how it ends

THREE WAYS OUT. ONE LINE COVERS ALL OF THEM.reaches the enderror under set -eCtrl-C, TERMthe one linetrap 'rm -f $LOCK' EXITcleanuplockfiletemp diralwayssweptthe happy path is the one that least needs sweeping

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.

TRY IT THIS WEEK

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.

Shell · No. 05
Plumbing

2>&1, and friends

Three channels, pointed where you want them

REDIRECT IS A POINTER. ORDER IS THE WHOLE SENTENCE.cmdstdout1 > loglogstderr2 > loglogthe ordercmd >log 2>&1cmd 2>&1 >logerrors stayon terminalboth at once&> logBash shorthand,both channels

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.

TRY IT THIS WEEK

Run ls . missing 2>&1 >out.txt and then ls . missing >out.txt 2>&1. Compare what hits the terminal.

Index

Index


2>&1, and friends9
pipefail7
Quoting "$var"4
set -e and -u6
trap on EXIT8