Exercise: Make It Fail Well

Turn a photo mover that works only on a good day into one you can trust: strict mode, a die function, a usage message, errors on standard error, exit codes that mean something, cleanup with trap, a bug found with bash -x, and two bats tests of your own, while a check script runs it job by job.

Journeyman · Unix · Linux · Updated

Sam's new phone and old phone both number their photos from the same place, so every evening mover.sh moves the day's photos from inbox into photos and must never let one replace another. On a good day it works. On a bad day it says the wrong things in the wrong place, reports success when it failed, leaves half-copied files behind, and has a bug that loses a photo that was already there. You'll make it fail well, the way an application people depend on should, and then write two tests of your own. A check script runs your script for real, job by job, and tells you what's done.

Before you start

You need a terminal: the Terminal app on a Mac, or Ubuntu on Linux or in WSL on Windows. The check runs with bats, from Testing Scripts with bats. If bats --version doesn't print a version number yet, install it:

sudo apt install bats       # Ubuntu, and Ubuntu in WSL
brew install bats-core      # a Mac, with Homebrew

sudo asks for your password, the one you use to sign in; nothing shows on the screen while you type it.

Get the exercise code

Open this lesson's GitHub repository, github.com/ask-codey/exercise-bash-make-it-fail-well, also linked from the Exercise box beside the lesson. Click Code, then Download ZIP, and unzip it into your terminal-practice folder. Or clone it with Git.

The folder holds:

  • mover.sh: the mover to finish, with eleven jobs, each a comment saying what to do and exactly what to print.
  • my-tests.bats: one test for the mover, and room for two of yours.
  • inbox and photos: practice photos, each a one-line text file. photos/IMG_0412.jpg is a birthday cake that must survive.
  • check.bats: the check. You don't need to change it.

As it is, the starter can lose the practice photos, for example when it's given a TO folder that doesn't exist. That's what you're fixing; if it happens, download the exercise again.

What to do

cd into the exercise folder, run chmod +x mover.sh, and open it with nano mover.sh. Each job is a comment, with room under it for your lines:

# 1. Turn on strict mode.


usage="Usage: mover.sh FROM-FOLDER TO-FOLDER"

# 2. Write a die function: it prints "mover: " and its message on standard
#    error, then stops the script with exit code 1.
  1. Strict mode, on its own line (Strict Mode: set -euo pipefail).
  2. A die function that prints on standard error and stops with exit code 1 (Errors a User Can Act On).
  3. --help: the usage line on standard output, exit code 0.
  4. Run wrong, with anything but two folders: the usage line on standard error, exit code 2.
  5. Missing folders: die with the job's message for each.
  6. Clean up: tmp="" and a trap that removes "$tmp", however the script ends (Cleaning Up: trap and mktemp).
  7. The bug: a photo already in the TO folder gets replaced. Find why with bash -x (Debugging Scripts: bash -x), and fix it.
  8. Skipped photos: the warning on standard error, and counted.
  9. A failed copy: die with the job's message, keeping the original.
  10. The summary: Done: N moved, M skipped., then exit code 1 when anything was skipped.
  11. Your tests: two more tests in my-tests.bats (Testing Scripts with bats).

Each job's comment gives its message word for word. Save, and try your script as you go: ./mover.sh, ./mover.sh inbx photos, then ./mover.sh inbox photos.

Check your work

From the exercise folder:

bats check.bats

At first most checks fail. That's expected: each job you finish turns its check into a ✓. A failing check says what it expected on each stream, what your script printed there, and what to look at, for example The warning was printed on standard output. Add >&2 to its echo (job 8). The check runs your script in a throwaway folder of its own, with made-up photos, so the practice photos in your folder are never touched. bats my-tests.bats runs your own tests alone.

Hints

  • Work from the top. Strict mode first, so every later job is tested the way the finished script runs.
  • Strict mode's surprises will show up: "$1" with no arguments needs "${1:-}", and ((moved++)) stops the script when moved is 0, so write moved=$((moved + 1)).
  • || die reads "or else die": [[ -d "$src" ]] || die "...", and the same after the cp line.
  • The trap's command goes in single quotes, above the loop, after tmp="".
  • For job 7, run bash -x ./mover.sh inbox photos on a fresh copy of the practice photos and read the line that starts + [[ -e. Which path did it really check?
  • For job 11, copy the first test in my-tests.bats and change it. A photo that's already there is one printf line before run.
  • If you have ShellCheck, run shellcheck mover.sh when you're done, and read anything it has to say.

When you're done

Every check passes:

check.bats
 ✓ the script is here and bash can read it
 ✓ 1) strict mode is on
 ✓ 3) --help prints the usage line on standard output
 ✓ 4) run with nothing, it prints the usage line on standard error
 ✓ 4) run with one folder, it prints the usage line too
 ✓ 2) 5) a missing FROM folder: die says so
 ✓ 2) 5) a missing TO folder: die says so
 ✓ 7) 8) never replaces a photo that's already there
 ✓ 8) 10) moves every photo and says how many
 ✓ 8) 10) a skipped photo is counted, and the exit code says so
 ✓ 6) 9) a failed copy: it says so and leaves no half-copied file
 ✓ 11) your own tests: at least three, and they pass

12 tests, 0 failures

And by hand, in the exercise folder:

$ ./mover.sh
Usage: mover.sh FROM-FOLDER TO-FOLDER
$ ./mover.sh inbx photos
mover: there's no folder called 'inbx'. Check the name with ls.
$ ./mover.sh inbox photos
mover: skipped IMG_0412.jpg: photos already has one.
Moved IMG_0413.jpg.
Moved IMG_0414.jpg.
Moved beach day.jpg.
Done: 3 moved, 1 skipped.

Now think about who runs this script at night, when nobody is watching. What does exit code 1 let a nightly job do that the starter's exit code 0 never could?

Submit your exercise

The exercise is how you finish this lesson. When your work is done, put it on GitHub in a repo of your own, then submit that repo's address here.

  1. On GitHub, click +, then New repository. Name it (for example exercise-bash-make-it-fail-well), choose Public, and click Create repository. Leave out the README: your folder already has one.

  2. In a terminal in your exercise folder (Ubuntu in WSL on Windows), send your work to your new repo. If you downloaded the ZIP:

    git init -b main
    git add .
    git commit -m "My solution"
    git remote add origin https://github.com/your-name/exercise-bash-make-it-fail-well.git
    git push -u origin main
    

    If you cloned the starter instead, point your copy at your own repo, then commit and push:

    git remote set-url origin https://github.com/your-name/exercise-bash-make-it-fail-well.git
    git add .
    git commit -m "My solution"
    git push -u origin main
    
  3. Paste your repo's address, such as https://github.com/your-name/exercise-bash-make-it-fail-well, into Submit your exercise on this page. The site checks that the repo is public and holds your commits, and then the lesson is complete.

Changed something after you submitted? Push it, and submit again: your latest submission is the one that counts.

So what

Your mover now behaves like software people can depend on: it explains itself, puts problems where they belong, reports failure honestly, tidies up after an interruption, and comes with tests that prove it.

Key terms

Related