Strict Mode: set -euo pipefail
set -euo pipefail makes a script stop at the first sign of trouble: -e stops at a command that fails, -u at a variable that was never set, and -o pipefail at a pipeline with a failing part, with a few surprises to know about: a grep that finds nothing, ((count++)) and a missing $1.
Freelancer · Unix · Linux · Updated
After this lesson you can
- Turn on strict mode with set -euo pipefail, and say what -e, -u and -o pipefail each do
- Work around strict mode's surprises in a script, such as a grep that finds nothing, ((count++)) and a missing $1
Key takeaways
- -e stops the script at the first command that fails, instead of carrying on in the wrong folder or with the wrong data.
- -u makes a variable that was never set an error, so a typo such as $photo_dri stops the script instead of becoming nothing.
- -o pipefail makes a pipeline fail when any part of it fails, not only the last command.
- The surprises: grep that finds nothing fails, ((count++)) fails when count is 0, and "$1" fails when there's no argument. Write || true, count=$((count + 1)) and "${1:-}".
- Strict mode is for scripts. A failing test after if, or a command before || or &&, doesn't stop the script.
A script runs every line you give it, even after one has failed. That's like a cook who finds the oven broken and carries on, putting the cake mix in a cold oven and setting the timer anyway. If the failed line was a cd, every line after it works in the wrong folder; if it was a typo in a variable's name, the script works on nothing at all. Careful scripts start with one line that changes this, set -euo pipefail, often called strict mode: stop at the first sign of trouble. Administrators put it at the top of deployment and backup scripts, where carrying on after a failure does real damage.
The examples are small scripts in the mover practice folder from Errors a User Can Act On. Make each with nano, chmod +x it, and run it.
-e: stop at the first failure
tidy.sh is meant to tidy the inbox, but the folder's name is mistyped:
#!/usr/bin/env bash
cd inbx
echo "Tidying $(pwd)"
./tidy.sh
echo $?
./tidy.sh: line 2: cd: inbx: No such file or directory
Tidying /home/sam/terminal-practice/mover
0
cd failed, and the script carried on in the wrong folder, then reported success. If the next line deleted old files, it would delete them there. Add set -e (read "set, exit on error") as the second line, and run it again:
./tidy.sh: line 3: cd: inbx: No such file or directory
1
It stopped at the failure, and its exit code says so.
-u: a misspelled variable is an error
In where.sh, the variable is photo_dir but the ls line says photo_dri:
#!/usr/bin/env bash
photo_dir=photos
ls "$photo_dri/"
./where.sh
bin dev home lib lost+found mnt proc run snap sys usr
boot etc init lib64 media opt root sbin srv tmp var
A variable that was never set counts as empty, so "$photo_dri/" became /, and ls listed the top of the whole computer. With rm in place of ls, that's how scripts have deleted far more than they meant to. Add set -u ("unset variables are errors") as the second line:
./where.sh: line 4: photo_dri: unbound variable
Unbound is bash's word for "never set".
-o pipefail: every part of a pipe counts
A pipeline's exit code is normally its last command's. In waiting.sh, with set -e already on, ls fails, but wc counts nothing happily:
#!/usr/bin/env bash
set -e
waiting=$(ls inbx | wc -l)
echo "$waiting photos are waiting."
./waiting.sh
ls: cannot access 'inbx': No such file or directory
0 photos are waiting.
Change the second line to set -eo pipefail (read "set option pipefail"), and the pipeline fails when any part of it fails: the script stops after ls's message, with exit code 2, ls's own. All three together are written set -euo pipefail, on the line after the comment at the top of a script.
The surprises
Strict mode is strict about some things that aren't mistakes:
- A
grepthat finds nothing gives exit code 1, sopngs=$(ls inbox | grep -c png)stops the script silently when there are no PNG files. Add|| true(read "or else, carry on"):pngs=$(ls inbox | grep -c png || true)prints0 PNG files. ((count++)), a shortcut for adding 1 that you'll see in other people's scripts, gives exit code 1 whencountwas 0, and the script stops silently after the first item. Writecount=$((count + 1)), which always works."$1"with no arguments is an unset variable. Here'smover.shwith strict mode on line 3, run with nothing:
./mover.sh: line 11: $1: unbound variable
The usage message never got its chance. Write "${1:-}" on that line (read "the value of 1, or nothing if there isn't one"):
if [[ "${1:-}" == "--help" ]]; then
./mover.sh
echo $?
Usage: mover.sh FROM-FOLDER TO-FOLDER
2
Two things don't stop a strict script, on purpose: a failing test after if or while, and a command before || or &&. That's why [[ -d "$src" ]] || die "..." still works. And keep strict mode in scripts: typed at your prompt, set -e closes the terminal at your first typo.
Check yourself
2 questions to try on the lesson page.
So what
set -euo pipefail turns "carry on and hope" into "stop and say why", and knowing its three surprises means it never stops a script that was doing the right thing.