Linux Basic β€” Reading & Writing Files Practice Exercises

4 minute read

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 FILE must never see a partially-written version
  • A crash mid-write must leave FILE either 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:

  1. All output to be saved to build.log
  2. Only error lines saved to errors.log
  3. 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 -d for a unique private workspace
  • trap 'rm -rf -- "$work"' EXIT guarantees cleanup
  • Parallel curl with & + wait
  • Final delivery via mv within the same filesystem (/tmp) so it’s atomic
  • set -euo pipefail makes any failure abort early (and trap still 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

Updated: