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.inboxandphotos: practice photos, each a one-line text file.photos/IMG_0412.jpgis 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.
- Strict mode, on its own line (Strict Mode: set -euo pipefail).
- A
diefunction that prints on standard error and stops with exit code 1 (Errors a User Can Act On). --help: the usage line on standard output, exit code 0.- Run wrong, with anything but two folders: the usage line on standard error, exit code 2.
- Missing folders:
diewith the job's message for each. - Clean up:
tmp=""and atrapthat removes"$tmp", however the script ends (Cleaning Up: trap and mktemp). - The bug: a photo already in the TO folder gets replaced. Find why with
bash -x(Debugging Scripts: bash -x), and fix it. - Skipped photos: the warning on standard error, and counted.
- A failed copy:
diewith the job's message, keeping the original. - The summary:
Done: N moved, M skipped., then exit code 1 when anything was skipped. - 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 whenmovedis 0, so writemoved=$((moved + 1)). || diereads "or else die":[[ -d "$src" ]] || die "...", and the same after thecpline.- The trap's command goes in single quotes, above the loop, after
tmp="". - For job 7, run
bash -x ./mover.sh inbox photoson 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.batsand change it. A photo that's already there is oneprintfline beforerun. - If you have ShellCheck, run
shellcheck mover.shwhen 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.
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.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 mainIf 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 mainPaste 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.