Putting it together: a log report
A capstone that uses the whole course once: build a data file, filter it with a pipeline, keep the answers in variables, and print a summary that lines up.
The shape of a small script
Eleven chapters of pieces, arranged into one file. The data is built first, so the script can be run anywhere and always has the same rows to work on. Then a pipeline answers the first question, three captured variables hold the numbers the summary needs, and the last four lines print it.
The order is the lesson. Build, then ask, then print. A script that prints while it is still working out its answers is the shape that becomes unreadable, because the meaning of a number on the screen is a decision that was made somewhere in the middle. The reader can stop after any section and know what the next one will use.
Filtering, counting, and printing
The filter is a pipeline of the tools from the last two chapters: grep picks one person, sort orders the rows, and the answer arrives already in the order the reader wants it. The counts use wc -l fed by the less-than sign, sort -u for distinct names, and awk for the total of a column — the decimal-friendly tool, though this data divides evenly enough that the shell could have done it. The pipeline is the answer to one question, not a general-purpose program.
The formatting is the last thing and nothing else is asked of it. printf '%-9s %s\n' prints a label padded to nine characters, then the value, so the columns sit under each other in a fixed-width font. Change a label and nothing else moves; that is what the padding is for. The width belongs to the label, not to the value, so the values still line up when they grow.
The pieces, named one at a time
# 1. Build the data, so the run never depends on a file.
printf '%s\n' "ada:alpha:42" "grace:beta:17" > /tmp/log.txt
# 2. Ask one question with a pipeline.
grep '^ada:' /tmp/log.txt | sort -t: -k3 -nr
# 3. Keep three answers in named variables.
rows=$(wc -l < /tmp/log.txt)
minutes=$(awk -F: '{ s += $3 } END { print s }' /tmp/log.txt)
people=$(cut -d: -f1 /tmp/log.txt | sort -u | wc -l)
# 4. Print a summary whose columns line up.
printf '%-9s %s\n' "people" "$people"
printf '%-9s %s\n' "minutes" "$minutes"
printf '%-9s %s\n' "average" "$((minutes / rows))"Guards that fail loudly
set -u makes an unset name an error instead of an empty string, set -e stops the script when a command fails, and set -o pipefail makes a pipeline report the first failing command rather than the last. Together they turn silent wrong answers into a stop, which is usually what a report script wants. The guards are a decision about how the script should fail, not a decoration.
Each has exceptions worth knowing. A command used as the condition of if or while is allowed to fail, and a command followed by || is handled, so set -e does not fire there. set -u still complains about ${1} when no argument was given, which is why a default such as ${1:-} is the usual guard. A guard that stops too eagerly is worse than no guard, so the exceptions are part of the design.
Making the answer checkable
A report is easier to trust when its data is built inside the script and its shape is fixed. Same rows in, same lines out, every run — then a change in the output is a change you made, not a difference in the input. That is also what makes an example you can practise on.
Keep each answer in a name before printing it. A number printed from the middle of a pipeline has no label, and a number that appears twice can disagree with itself; a variable is computed once, and the summary reads from it. The printing is then only formatting, which is the easiest part to change. A number kept in a name can be printed once, labelled, and reused without recomputing it.
What the script does not do
The script never reads a file it did not create, never asks for input, and never depends on the time of day. Those choices are what make the output stable enough to check. A report that reads the clock or the network is harder to test, because the same script can produce a different answer on two runs.
It also does not format while it computes. The arithmetic stays in whole numbers, and printf is the only place that decides how many columns a label needs. Keeping those jobs apart is what lets a later change to the layout leave the numbers untouched.
The report, in one file
Run it and read the four sections. Then add a fifth: the team totals with `cut` and `uniq -c`, and a line that prints the longest session on its own.
Notice what the script never does: it does not print the numbers it is still working on, and it does not repeat a pipeline it could keep in a variable. Small habits, and they are most of the difference between a script you can read next month and one you cannot.
Three capstone-sized traps: set -e does not fire inside an if condition, by design; an unset variable under set -u aborts the moment it is read, even if a later line would have set it; and (( count++ )) can report failure at zero under set -e. Write the guards deliberately, and test the script once with a missing argument.
Try these next
- Add a total-per-team section to the report with
cutanduniq -c. - Print the busiest person with
sort | uniq -c | sort -nr | head -n 1. - Add
set -uat the top, then print an unset name on purpose and read the error.
A report grouped by team
Run it and read the team totals. Then add a fifth row, and add a line that prints the team with the most minutes.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why does the script build its data file before asking any questions?
