Capstone: the station log report
Combine pipelines, loops, associative arrays, functions and strict mode into a real reporting tool.
- Break a reporting task into small, testable steps
- Choose between a pipeline and a Bash loop for each step
- Write a complete, robust script that turns a raw log into a status report
Incoming message from Commander Hopper:
Engineer - every morning I scroll through
station.logby hand to work out whether the station is on fire. I would like a report on my screen by 09:30 instead. Make it so.
Each log line has four parts - a time, a level (INFO, WARN or ERROR), a subsystem, and a free-text message:
08:09 ERROR reactor coolant pump stalledThe finished tool will print:
1=== KESTREL LOG REPORT ===
2Entries: 12 (08:00-09:15)
3ERROR: 5 WARN: 3 INFO: 4
4Busiest: reactor (5 entries)
5Latest error: 09:10 reactor: pressure spike
6Status: REDYou’ll build it in three exercises. First a quick pipeline, then a loop with associative arrays, then the full report.
Step 1: answer questions with pipelines
Before writing a big script, find out how far one-liners get you. They’re quick to try and easy to check. Counting lines per level is the frequency-count pattern from the pipelines lesson, with awk reformatting the result and adding a total:
1log='08:00 INFO galley coffee brewed
208:09 ERROR reactor coolant pump stalled
308:12 WARN comms signal weak
408:20 ERROR reactor pressure spike'
5cut -d' ' -f2 <<< "$log" | sort | uniq -c | sort -k1,1nr -k2,2 |
6 awk '{ print $2, $1; total += $1 } END { print "total", total }'ERROR 2 INFO 1 WARN 1 total 4
A pipeline can continue on the next line after a | - handy for long ones. sort -k1,1nr -k2,2 sorts by count (numerically, descending), then by name, so ties come out in a predictable order.
Step 2: one pass, many tallies
The full report needs several numbers at once: counts per level and per subsystem, the first and last times, and the latest error. Running a separate pipeline for each would read the log many times. Instead, read it once with while read, splitting each line into fields, and keep tallies in associative arrays:
1declare -A by_subsystem
2latest_error="none"
3while read -r time level subsystem message; do
4 (( by_subsystem[$subsystem] += 1 ))
5 [[ $level == ERROR ]] && latest_error="$time $subsystem: $message"
6done <<'EOF'
708:09 ERROR reactor coolant pump stalled
808:12 INFO galley coffee brewed
908:20 ERROR comms antenna misaligned
1008:31 INFO reactor temperature stable
11EOF
12for name in "${!by_subsystem[@]}"; do echo "$name ${by_subsystem[$name]}"; done | sort
13echo "latest error: $latest_error"comms 1 galley 1 reactor 2 latest error: 08:20 comms: antenna misaligned
read -r time level subsystem message puts the first three words in their own variables and everything else in message, so multi-word messages survive intact.
Step 3: find the busiest subsystem without a pipeline
To pick the subsystem with the most entries, you could pipe the tallies through sort | head -n 1. But under set -o pipefail, head closing the pipe early can occasionally make sort fail with SIGPIPE - and with set -e, that ends your script. A small loop avoids the problem, and breaks ties alphabetically:
1set -euo pipefail
2declare -A tally=([galley]=3 [comms]=5 [reactor]=5)
3busiest=""
4for name in "${!tally[@]}"; do
5 if [[ -z $busiest ]] || (( tally[$name] > tally[$busiest] )) ||
6 { (( tally[$name] == tally[$busiest] )) && [[ $name < $busiest ]]; }; then
7 busiest=$name
8 fi
9done
10echo "Busiest: $busiest (${tally[$busiest]} entries)"Busiest: comms (5 entries)
Where to go next
You can now automate almost anything you can type. Some ideas for what to learn next:
- cron and systemd timers - run your report every morning at 09:25.
- ssh and rsync - run commands and copy files on other machines.
- tmux - keep sessions running and split your terminal into panes.
- jq - a sed-and-awk for JSON, the language of most APIs.
- make - describe how to build things once, and rebuild only what changed.
And whenever a script outgrows a page or two, or needs real data structures, consider moving to Python. Knowing when to switch is part of shell wisdom too.
Key takeaways
Start with pipelines to explore; switch to a single
while readpass when you need many numbers at once.Associative arrays turn a loop into a set of tallies.
Under strict mode, avoid fragile
| headpipelines in substitutions and trailing&&decisions.Test scripts with small, tricky inputs - including the boring “nothing went wrong” case.
Lesson quiz
6 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: write Bash scripts
Write a script in the editor and run it for real against sample input. Each run gets a fresh Linux sandbox with Bash 5.2 and the GNU tools on Wandbox, a free public service - so experiment freely, even with rm. Your script and test input are sent there.
Level summary
stdin is a station log. Print each level with its count, most frequent first (ties alphabetically), then total and the number of lines:
1ERROR 5
2INFO 4
3WARN 3
4total 12- Morning log
- A tie
Your script runs with Bash 5.2 and GNU tools on Wandbox, a free public service, in a fresh sandbox each time. Your script and test input are sent to that service.
Error hotspots
stdin is a station log. For each subsystem with at least one ERROR, print its name, its error count and the times of its errors in log order, like reactor 3: 08:09 08:40 09:10. List the subsystem with the most errors first; break ties alphabetically.
- Morning log
- An outage
Your script runs with Bash 5.2 and GNU tools on Wandbox, a free public service, in a fresh sandbox each time. Your script and test input are sent to that service.
The full report
Write the complete report for Commander Hopper. stdin is a station log; print:
1=== KESTREL LOG REPORT ===
2Entries: 12 (08:00-09:15)
3ERROR: 5 WARN: 3 INFO: 4
4Busiest: reactor (5 entries)
5Latest error: 09:10 reactor: pressure spike
6Status: RED- Entries: the number of lines, with the first and last times.
- The level counts always show all three levels, even when a count is 0. (Note the two spaces between them.)
- Busiest: the subsystem with the most entries of any level; ties go to the alphabetically first name.
- Latest error:
time subsystem: messagefor the last ERROR line, ornone. - Status:
REDif there are any errors, otherwiseAMBERif there are any warnings, otherwiseGREEN.
The starter has strict mode switched on - keep it, and make sure your script still exits with status 0.
- Morning log
- A calm afternoon
- Warnings and a tie
- An outage
Your script runs with Bash 5.2 and GNU tools on Wandbox, a free public service, in a fresh sandbox each time. Your script and test input are sent to that service.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…