Linux Basic β Reading & Writing Files Practice Exercises
Reading & Writing Files β Practice Exercises
These exercises cover the full chapter: safe reads, predictable writes, robust appends, fan-out with tee, and race-free temp files. Each comes with a hidden answer β try it yourself first.
Exercise 1 β Count Real Lines
Goal: Read a file line-by-line and count every line, including blanks and lines with leading whitespace.
Setup:
cat > /tmp/ex1.txt <<'EOF'
hello world
indented line
file with *
last line
EOF
Task: Write a snippet that prints Total: N where N is the true number of lines, then re-runs wc -l for comparison. Both should agree.
π‘ Show Answer
count=0
while IFS= read -r line; do
((count++))
done < /tmp/ex1.txt
echo "Total: $count"
# Total: 5
wc -l < /tmp/ex1.txt
# 5
If wc -l reports 4 and your loop reports 5, the file is missing the trailing newline β fix it with [ "$(tail -c1 /tmp/ex1.txt)" != $'\n' ] && echo >> /tmp/ex1.txt.
Exercise 2 β Atomic Config Writer
Goal: Write a function safe_write FILE that reads stdin and writes it to FILE atomically.
Requirements:
- A reader of
FILEmust never see a partially-written version - A crash mid-write must leave
FILEeither fully old or fully new (never half) - The tempfile must be in the same directory as
FILE
π‘ Show Answer
safe_write() {
local dest=$1
local dir
dir=$(dirname -- "$dest")
local tmp
tmp=$(mktemp "$dir/.$(basename -- "$dest").XXXXXX")
trap 'rm -f -- "$tmp"' RETURN
cat > "$tmp" || return 1
mv -- "$tmp" "$dest"
}
# Usage
printf '%s\n' "key=value" "other=42" | safe_write /tmp/app.conf
cat /tmp/app.conf
# key=value
# other=42
Key points: tempfile in the same directory (so mv is a rename, not a cross-FS copy), trap ... RETURN to clean up on failure, mv for the atomic swap.
Exercise 3 β Live Build Log + Error Highlight
Goal: Run a fake build that emits both info and error lines, and arrange for:
- All output to be saved to
build.log - Only error lines saved to
errors.log - The terminal to show only error lines (so the human sees what matters)
You may use tee, process substitution, and grep.
Setup:
fake_build() {
echo "INFO: starting"
echo "INFO: compiling module A"
echo "ERROR: missing import in A"
echo "INFO: compiling module B"
echo "ERROR: type mismatch in B"
echo "INFO: linking"
}
π‘ Show Answer
fake_build 2>&1 \
| tee build.log \
>(grep '^ERROR' > errors.log) \
| grep '^ERROR'
# Terminal shows:
# ERROR: missing import in A
# ERROR: type mismatch in B
cat build.log
# INFO: starting
# INFO: compiling module A
# ERROR: missing import in A
# INFO: compiling module B
# ERROR: type mismatch in B
# INFO: linking
cat errors.log
# ERROR: missing import in A
# ERROR: type mismatch in B
The first tee captures everything to build.log, the process substitution >(grep ... > errors.log) saves only errors, and the trailing grep filters what reaches the terminal.
Exercise 4 β Concurrent Log Writer
Goal: Two background loops both append to /tmp/shared.log 100 times each, with timestamps. Show that without locking the lines can interleave, and that with flock they donβt.
π‘ Show Answer
# β Without flock
log_unsafe() {
for i in $(seq 1 100); do
printf '[%s] %s line %d\n' "$(date +%T.%N)" "$1" "$i" >> /tmp/shared.log
done
}
> /tmp/shared.log
log_unsafe A &
log_unsafe B &
wait
# Inspect β you'll likely see lines from A and B interleaved cleanly
# (Bash's printf usually atomic <4KB), but on long lines you'd see corruption.
wc -l /tmp/shared.log
# 200
# β
With flock
log_safe() {
for i in $(seq 1 100); do
flock /tmp/shared.lock -c "printf '[%s] %s line %d\n' \"\$(date +%T.%N)\" \"$1\" \"$i\" >> /tmp/shared.log"
done
}
> /tmp/shared.log
log_safe A &
log_safe B &
wait
wc -l /tmp/shared.log
# 200
A more performant variant locks once per worker:
log_safe_batch() {
(
flock -x 9
for i in $(seq 1 100); do
printf '[%s] %s line %d\n' "$(date +%T.%N)" "$1" "$i"
done >> /tmp/shared.log
) 9> /tmp/shared.lock
}
Exercise 5 β Race-Free Workspace
Goal: Write a script that downloads three URLs in parallel into a private workspace, joins their contents into one file, and delivers the result atomically to /tmp/joined.txt. The workspace must be cleaned up no matter what.
π‘ Show Answer
#!/usr/bin/env bash
set -euo pipefail
work=$(mktemp -d -t multidl.XXXXXX)
trap 'rm -rf -- "$work"' EXIT
urls=(
"https://example.com/"
"https://www.iana.org/"
"https://httpbin.org/get"
)
# Download in parallel
i=0
for url in "${urls[@]}"; do
curl -fsSL "$url" -o "$work/part.$i" &
((i++))
done
wait
# Join, then deliver atomically (same FS as target)
cat "$work"/part.* > "$work/joined.txt"
mv -- "$work/joined.txt" /tmp/joined.txt
echo "Done β $(wc -l < /tmp/joined.txt) lines"
Highlights:
mktemp -dfor a unique private workspacetrap 'rm -rf -- "$work"' EXITguarantees cleanup- Parallel
curlwith&+wait - Final delivery via
mvwithin the same filesystem (/tmp) so itβs atomic set -euo pipefailmakes any failure abort early (andtrapstill cleans up)
Whatβs Next?
You can now read and write files safely. The next chapter covers file existence checks, permissions, and defensive scripting β how to verify that a file is actually readable/writable/owned by the right user before you touch it.
Continue β File Existence & Permissions