Linux Basic — Debugging Techniques Practice Exercises
Debugging Techniques — Practice Exercises
Five exercises across tracing, ShellCheck, debug modes, and the classic bug-hunt.
Exercise 1 — Trace With Custom PS4
Goal: Set a PS4 that includes the filename (basename only), line number, and function name. Then trace this snippet and capture the output:
greet() {
local name=$1
echo "Hello, $name"
}
set -x
greet Alice
greet Bob
💡 Show Answer
#!/usr/bin/env bash
export PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '
greet() {
local name=$1
echo "Hello, $name"
}
set -x
greet Alice
greet Bob
Run and capture:
$ bash script.sh 2> trace.log
Hello, Alice
Hello, Bob
$ cat trace.log
+ script.sh:9:main: greet Alice
+ script.sh:4:greet: local name=Alice
+ script.sh:5:greet: echo 'Hello, Alice'
+ script.sh:10:main: greet Bob
+ script.sh:4:greet: local name=Bob
+ script.sh:5:greet: echo 'Hello, Bob'
Three things to remember:
export PS4=…makes the new prompt take effect (also subshells)${BASH_SOURCE##*/}is “basename of current source file” — no fork${FUNCNAME[0]:-main}falls back tomainoutside any function (FUNCNAME is empty in top-level code)
Exercise 2 — Fix a Broken Script With ShellCheck
Goal: This script has at least 5 bugs that ShellCheck will catch. Run shellcheck on it, fix every warning, and end up with a script that handles filenames with spaces correctly.
#!/bin/sh
DIR=$1
cd $DIR
files=`ls *.log`
for f in $files; do
if [ -s $f ]; then
wc -l $f
fi
done
💡 Show Answer
Issues ShellCheck flags:
- SC2086 —
$DIRand$fshould be quoted - SC2006 — use
$(...)not backticks - SC2010/SC2207 — don’t parse
ls; use glob + array - SC2164 —
cdwithout|| exit - SC2148 —
#!/bin/shbut using bashisms? Be explicit - SC2128 —
$filesis a single string treated as an array
Fixed version:
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
dir=${1:?Usage: $0 <dir>}
cd -- "$dir" || { echo "Cannot cd to $dir" >&2; exit 1; }
files=( *.log )
(( ${#files[@]} )) || { echo "No .log files in $dir" >&2; exit 0; }
for f in "${files[@]}"; do
[[ -s "$f" ]] || continue
wc -l -- "$f"
done
Improvements:
set -euo pipefailstrict modeshopt -s nullglobso empty glob doesn’t iterate literal*.log${1:?…}required-arg check via parameter expansioncd … || exitwith friendly message- Quoted
"$dir"and"$f" --separator oncdandwcin case of path starting with-- Array for glob results
- Explicit empty-array check with a useful message
Run shellcheck fixed.sh → no warnings.
Exercise 3 — Build a DEBUG-Aware Logger
Goal: Implement three log functions:
info()— always prints to stderr withINFO:prefixdebug()— prints only whenDEBUG=1, withDEBUG:prefixtrace()— prints only whenDEBUG=2, withTRACE:prefix
All three should accept any number of arguments and join with spaces.
Demonstrate by calling each with the same message under DEBUG unset, DEBUG=1, and DEBUG=2.
💡 Show Answer
#!/usr/bin/env bash
set -euo pipefail
LOG_LEVEL=${DEBUG:-0}
info() { printf 'INFO: %s\n' "$*" >&2; }
debug() { (( LOG_LEVEL >= 1 )) || return 0; printf 'DEBUG: %s\n' "$*" >&2; }
trace() { (( LOG_LEVEL >= 2 )) || return 0; printf 'TRACE: %s\n' "$*" >&2; }
info "Always visible"
debug "Visible at DEBUG=1+"
trace "Visible only at DEBUG=2"
Test all three:
$ ./script.sh
INFO: Always visible
$ DEBUG=1 ./script.sh
INFO: Always visible
DEBUG: Visible at DEBUG=1+
$ DEBUG=2 ./script.sh
INFO: Always visible
DEBUG: Visible at DEBUG=1+
TRACE: Visible only at DEBUG=2
Three things to notice:
- All output goes to stderr (
>&2) — keeps stdout clean for data (( LOG_LEVEL >= N )) || return 0— early-return when disabled costs almost nothing"$*"joins args with a single space (uses$IFS’s first char — usually a space)
For a one-letter CLI variant:
while [[ $# -gt 0 ]]; do
case "$1" in
-v) LOG_LEVEL=1; shift ;;
-vv) LOG_LEVEL=2; shift ;;
*) break ;;
esac
done
Exercise 4 — Diagnose: Why Is count 0?
Goal: This script should print 5, but prints 0. Explain why, then fix it without using shopt -s lastpipe.
#!/usr/bin/env bash
count=0
seq 1 5 | while read -r n; do
(( count++ ))
done
echo "$count"
💡 Show Answer
Why it fails: The while loop runs in a subshell (the right-hand side of a pipe). (( count++ )) increments count inside the subshell. When the subshell ends, that local change vanishes. The outer count is still 0.
Fix with process substitution:
#!/usr/bin/env bash
count=0
while read -r n; do
(( count++ ))
done < <(seq 1 5)
echo "$count"
# 5
< <(cmd) reads stdin directly from a pipe to cmd. The while loop runs in the current shell — no subshell, so count++ persists.
Other fixes:
# A. Direct file redirect (no subshell)
seq 1 5 > /tmp/nums
count=0
while read -r n; do (( count++ )); done < /tmp/nums
# B. Capture output, then iterate
count=0
output=$(seq 1 5)
while read -r n; do (( count++ )); done <<< "$output"
# C. mapfile + count array
mapfile -t nums < <(seq 1 5)
count=${#nums[@]}
The mapfile + array form is often the cleanest when you don’t actually need a while loop.
Exercise 5 — Capture Trace to a Separate File
Goal: Run a script in trace mode but route the trace output to /tmp/myscript.trace while keeping stdout and stderr clean for normal output.
Test script:
#!/usr/bin/env bash
echo "normal stdout"
echo "normal stderr" >&2
The trace file should contain the + echo … lines. Stdout and stderr should NOT include the trace.
💡 Show Answer
#!/usr/bin/env bash
# Open fd 3 to the trace file
exec 3>/tmp/myscript.trace
# Direct xtrace to fd 3
export BASH_XTRACEFD=3
export PS4='+ ${BASH_SOURCE##*/}:${LINENO}: '
set -x
echo "normal stdout"
echo "normal stderr" >&2
Test:
$ ./script.sh
normal stdout
normal stderr
$ cat /tmp/myscript.trace
+ script.sh:9: echo 'normal stdout'
+ script.sh:10: echo 'normal stderr'
Stdout/stderr are clean. The trace lives in its own file.
BASH_XTRACEFD is the right knob — without it, traces always go to stderr and you’d see them on the console.
If you want trace + normal output both on the console while ALSO saving the trace:
exec 3> >(tee -a /tmp/myscript.trace >&2)
export BASH_XTRACEFD=3
set -x
>(cmd) is process substitution; tee -a … >&2 writes to both the trace file and stderr.
What’s Next?
You have the full diagnostic toolkit: strict mode, traps, traces, ShellCheck, debug modes, and the catalog of common bugs. The next chapter moves into advanced scripting primitives — here-documents and here-strings, process substitution, subshells, command grouping, eval, coprocesses, and exec.
Continue → Advanced Scripting