Errors a User Can Act On

A script people depend on says what went wrong and what to do about it: it prints problems on standard error with >&2, stops with an exit code that means something (0 it worked, 1 it failed, 2 it was run wrong), and keeps those habits in a die function and a usage message.

Journeyman · Unix · Linux · Updated

After this lesson you can

  • Send a script's error messages to standard error with >&2, and stop with an exit code that says what happened
  • Write a die function and a usage message that tell the user what went wrong and what to do

Key takeaways

  • A good error message says who is talking, what went wrong and what to do next: mover: there's no folder called 'inbx'. Check the name with ls.
  • echo "..." >&2 prints on standard error, so a problem still reaches the screen when the normal output goes into a file or a pipe.
  • Exit codes tell the next command what happened: 0 for it worked, 1 for it failed, and, by custom, 2 for it was run the wrong way.
  • A die function prints its message on standard error and stops the script with exit 1, so each check fits on one line.
  • A usage message shows how to run the script: on standard error when it was run wrong, on standard output when someone asks with --help.

Your phone and your partner's phone both number their photos, so both have an IMG_0412.jpg. A script that moves the day's new photos into your photo folder has to notice that, or one photo silently replaces the other. And when something does go wrong, the only clue you get is what the script said. A car's dashboard doesn't light up "Error"; it says "Low tire pressure: front left". Scripts that people depend on, from installers to backup tools, fail the same way: clearly, with a next step, and with an exit code another program can check. This chapter's example is mover.sh, a file-mover that must never lose a file.

A mover that fails badly

These lines make the practice folder: three new photos in inbox, and last year's birthday cake already in photos as IMG_0412.jpg. Each photo is a one-line text file, so cat shows which is which. The second line deletes inbox and photos inside the practice folder only, so you get a fresh start every time you run them:

mkdir -p ~/terminal-practice/mover && cd ~/terminal-practice/mover
rm -rf ~/terminal-practice/mover/inbox ~/terminal-practice/mover/photos
mkdir inbox photos
printf 'beach\n' > inbox/IMG_0412.jpg
printf 'sunset\n' > inbox/IMG_0413.jpg
printf 'dog\n' > inbox/IMG_0414.jpg
printf 'cake\n' > photos/IMG_0412.jpg

Here's a first mover.sh, the way many scripts start. basename prints the last part of a path, the file's own name:

#!/usr/bin/env bash
# mover.sh: moves every file from one folder into another, never replacing one.
src="$1"
dest="$2"
for file in "$src"/*; do
  name=$(basename "$file")
  if [[ -e "$dest/$name" ]]; then
    echo "Skipped $name: $dest already has one."
  else
    mv "$file" "$dest/"
    echo "Moved $name."
  fi
done

On a good day it works, and the cake survives. On a bad day, you mistype a folder:

./mover.sh inbx photos
echo $?
mv: cannot stat 'inbx/*': No such file or directory
Moved *.
0

mv complained about a name you never typed, the script claimed it moved something, and the exit code says all is well. Run it with nothing at all, and it goes looking at the top of the whole computer (/, since "$src"/* became /*):

./mover.sh | head -n 3
Skipped bin:  already has one.
Skipped boot:  already has one.
Skipped dev:  already has one.

Who, what, and what to do

A useful error message has three parts: who is talking (mover:), what went wrong (there's no folder called inbx), and what to do next (check the name with ls).

It also goes out the right door. Three Streams: Output, Errors and Input showed that a command has two ways out: standard output for results and standard error for problems. echo prints on standard output, so add >&2 (read "into standard error"; &2 means stream 2, not a file named 2). Then ./mover.sh inbox photos > moved.log keeps a clean log while problems still reach your screen.

And it stops with an exit code that means something (Did It Work? Exit Codes, && and ||):

Code Means mover.sh uses it when
0 it worked every photo moved
1 it failed a folder is missing, or a photo was skipped
2 it was run the wrong way it didn't get two folders

2 for "used wrong" is a custom many commands follow: ls with an option it doesn't know gives 2.

A die function and a usage message

A usage message is one line that shows how to run the script. A die function, a name many scripts use, prints a message on standard error and stops (Functions: Naming a Job). Replace mover.sh with this:

#!/usr/bin/env bash
# mover.sh: moves every file from one folder into another, never replacing one.
usage="Usage: mover.sh FROM-FOLDER TO-FOLDER"

die() {
  echo "mover: $1" >&2
  exit 1
}

if [[ "$1" == "--help" ]]; then
  echo "$usage"
  exit 0
fi
if [[ $# -ne 2 ]]; then
  echo "$usage" >&2
  exit 2
fi
src="$1"
dest="$2"
[[ -d "$src" ]] || die "there's no folder called '$src'. Check the name with ls."
[[ -d "$dest" ]] || die "there's no folder called '$dest'. Make it with mkdir, or check the name."

skipped=0
for file in "$src"/*; do
  [[ -e "$file" ]] || continue
  name=$(basename "$file")
  if [[ -e "$dest/$name" ]]; then
    echo "mover: skipped $name: $dest already has one." >&2
    skipped=$((skipped + 1))
  else
    mv "$file" "$dest/"
    echo "Moved $name."
  fi
done
if [[ $skipped -gt 0 ]]; then
  exit 1
fi

--help prints the usage on standard output with exit 0, because asking isn't a mistake. || die reads "or else die". [[ -e "$file" ]] || continue skips the leftover pattern inbox/* when the folder is empty.

Run it wrong on purpose

Run the practice lines again for a fresh folder, then:

./mover.sh
echo $?
./mover.sh inbx photos
echo $?
./mover.sh inbox photos > moved.log
echo $?
cat moved.log
Usage: mover.sh FROM-FOLDER TO-FOLDER
2
mover: there's no folder called 'inbx'. Check the name with ls.
1
mover: skipped IMG_0412.jpg: photos already has one.
1
Moved IMG_0413.jpg.
Moved IMG_0414.jpg.

The warning reached the screen, the log holds only what moved, and exit code 1 tells a nightly job that something needs a look. cat photos/IMG_0412.jpg still says cake.

Check yourself

2 questions to try on the lesson page.

So what

A script that fails well tells you what happened and what to do, in the right place, with a code other scripts can trust: the first step from a list of commands to a tool people rely on.

Key terms

Cheat sheets

Related