Skip to content

Exercise 10 of 10 · Putting it together

Library Overdue Report

What you will make

A seven-line report: one aligned row per book with title, days overdue, fine at 25p a day and the action to take, followed by the total owed, how many books were on time, and the most overdue title found with a sort pipeline.

The one new idea: IFS=, read splits a comma-separated line into fields for that one read

This is what real shell scripts look like: a data file, a loop over its lines, a couple of helper functions, a case to classify, arithmetic to total, printf to line things up and pipelines for the summary. The one new piece, IFS=, in front of read, is the standard way to pull fields out of comma-separated data without any extra tool.

Go straight to the code ↓

Gluing it all together

This last exercise is a small report script, and every piece of it is something you have already used: a here-document to build a data file, two functions, a case, a while read loop, arithmetic, printf, and a couple of pipelines at the end. The one new idea is how to read a line that has several fields in it.

read splits each line into words using the characters in IFS — by default, spaces and tabs. If your file uses commas, tell read to split on commas instead, just for that one command, by putting IFS=, in front of it:

Shell
while IFS=, read -r who kg; do

Because the assignment is written on the same line as read, it applies only to read; the rest of the script keeps its normal word splitting. Without it, Dune,3 lands in the first variable as one word and the second variable is empty — and a title with a space in it, like The Hobbit,0, gets split in the wrong place entirely.

Two other idioms to notice: sort -t, -k2 -rn sorts on the second comma-separated field as a number, largest first; and cut -d, -f1 keeps only the first comma-separated field.

A worked example

Shell
cat > parcels.txt <<'END'
Ana,2
Bo,5
END
cost() {
  echo $(( $1 * 3 ))
}
while IFS=, read -r who kg; do
  c=$(cost "$kg")
  case "$kg" in
    [0-3]) size=small ;;
    *)     size=large ;;
  esac
  printf "%-4s %2d kg %3dp %s\n" "$who" "$kg" "$c" "$size"
done < parcels.txt
echo "Heaviest: $(sort -t, -k2 -rn parcels.txt | head -1 | cut -d, -f1)"
Output
Ana   2 kg   6p small
Bo    5 kg  15p large
Heaviest: Bo

Each line is split on the comma, the function's printed result is captured with $(...), the case picks a size, and printf lines the columns up. The final pipeline sorts by weight, keeps the top line, and trims it to the name.

Your turn

The editor holds a library overdue report. loans.txt has four books with the number of days each is overdue. For each book the script should print the title, days, fine at 25p a day, and the action: "on time" for 0 days, "reminder" for 1 to 6, "final notice" for more. Then it prints the total owed, how many were on time, and the most overdue title.

Two lines are marked. The loop's read has no IFS=,, so titles and days are not being separated. The fine is captured without $(...), so fine is never a number.

Fix both, and check every column of your output against the expected output — the widths in the printf are part of the answer.

If something goes wrong

If the titles print with ,3 still attached and the days column is all zeros, IFS=, is missing from the read line.

printf: fine_for: invalid number means the fine is not being captured. Wrap the function call in $(...).

If a title with a space in it comes out as just its first word, IFS=, is missing too — with default splitting, the space inside the title is what read cuts on.

Write your code

Runs in your browser. Press Run (or Ctrl/Cmd+Enter) and the output is checked for you.

Ctrl/Cmd+Enter to run

Press Esc then Tab to move keyboard focus out of the code editor.

Ready
Output will appear here after you run your code.

The runtime is starting in the background. You can type now — it will be ready before you are.

The answer appears here once you have run your code at least once.

Things that often go wrong here

Leaving IFS=, off the read line
With the default word splitting, a line like Dune,3 is one word, so title gets the whole thing and days is empty, while The Hobbit,0 splits on its space instead. Setting IFS=, just for read makes the comma the separator.
Setting IFS=, on its own line before the loop
That works, but it changes word splitting for the entire rest of the script, which can break later commands in surprising ways. Putting it in front of read limits the change to that one command.
Capturing a function's result without $(...)
fine=fine_for "$days" is an assignment prefix on a command called 3 or 12, so the shell tries to run 3 or 12 as a command and fails; fine itself is never set, and printf treats the empty value as 0. The call and its argument go inside $(...).
Using [1-6] and expecting it to match 12
[1-6] matches exactly one character, so 12 does not match it and correctly falls through to final notice. A pattern that matched two-digit numbers would need a second character class or a different approach.

Want a blank editor instead? Open the Bash & the Shell playground.