Skip to content
Lesson 11 of 14

Step 1 of 5 · Reading · ~4 min

Learn

Arrays and Scripts

Script Hygiene

By the end of this lesson you can turn on bash's strict mode, explain what each of its three switches protects you from, and write the few idioms that keep a strict script from dying on failures you expected. Every lesson so far showed how scripts work. This one is about how they fail silently, and the short preamble professionals paste at the top of every script to make failures loud, early and safe.

The unofficial strict mode

#!/usr/bin/env bash
set -euo pipefail

Three switches, each closing a real hole:

  • set -e: exit as soon as a command fails. By default bash keeps going after errors, so cd /nope; rm -rf * fails the cd and then runs the rm in the wrong directory. With -e the script stops at the cd.
  • set -u: using an unset variable is an error. By default a typo expands to an empty string, so rm -rf "$BUILD_DRI/" quietly becomes rm -rf "/". With -u bash stops with BUILD_DRI: unbound variable.
  • set -o pipefail: a pipeline fails if any stage fails. By default only the last stage counts, so grep x missing-file | sort reports success because sort worked.

set -e has exemptions: a command used as the condition of an if, while, && or || may fail without stopping the script. The pragmatic consensus is to use strict mode and learn the exemptions as you meet them. A script that dies too eagerly produces a bug report; a script that plows on produces an incident.

Strict mode makes normal failures fatal

Some tools use a non-zero status for an ordinary answer, and under -e that answer ends your script:

set -euo pipefail

hits=$(grep -c "timeout" server.log)      # no matches: grep prints 0 but EXITS 1, script dies here
echo "unreached"

When a failure is acceptable, say so explicitly. The idiom is || true, which runs true (always successful) only if the left side fails:

hits=$(grep -c "timeout" server.log || true)   # hits is 0, script continues

Do not write || echo 0 here. grep -c has already printed its own 0, so hits would hold the two lines 0 and 0.

A related trap: ((n++)) as a command has the status 1 when n was 0, because its value is 0 and arithmetic treats a zero result as "false". Under -e that kills the script on the first increment. Write n=$((n + 1)) instead.

For unset variables, -u is paired with expansions that supply a value:

name=${1:-guest}              # $1 if set and non-empty, otherwise guest
dir=${BACKUP_DIR:?set BACKUP_DIR first}   # fail with this message if unset or empty

Quote everything

You have heard it every lesson; here it is as policy: "$var", "${arr[@]}", "$@", always. Unquoted expansion with word-splitting and globbing is bash's largest bug class, and quoting is its complete cure. Related armour: mkdir -p (no error if the directory exists), rm -f (no error if the file is absent), and -- to end option parsing, so rm -- "$file" survives a file called -rf.

shellcheck: the reviewer that never sleeps

$ shellcheck deploy.sh

ShellCheck is a static analyser that knows the traps in this course and hundreds more, each with a page that explains why. It runs in editors, in CI and at shellcheck.net. Clean shellcheck output is the nearest bash gets to a compiler.

Fail with useful messages

die() {
    echo "ERROR: $*" >&2     # errors go to stderr, never into the results
    exit 1
}

[[ -f "$config" ]] || die "config not found: $config"

The message goes to stderr and the non-zero exit lets callers, and set -e, notice. $* joins all the arguments into one string.

Common mistakes

  • Strict mode without || true. The first grep that finds nothing ends the script.
  • || echo 0 after grep -c. It produces 0 twice.
  • ((n++)) from zero. It fails once, under -e, at the worst moment.
  • Testing an unset variable directly. Under -u, [[ -z $x ]] aborts when x is unset; use [[ -z ${x:-} ]].
  • Errors on stdout. A message in the middle of the results corrupts whatever consumes them.

Your exercise

Summarise a log under strict mode. The two questions you ask the log both fail with status 1 when the answer is "nothing found", which is a normal outcome here and must not stop the script. Decide for each command what failure means, and make the empty cases print a proper answer.

Up nextCommand SubstitutionCommand Substitution and Exit Status

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…