Debugging Bash Scripts — Techniques and Tools
Track down bugs in your Bash scripts with set -x trace output, LINENO, PS4, and common debugging patterns. Learn to read trace output and isolate failures.
Debugging Bash scripts is different from debugging compiled code — you can't attach a debugger. Instead, you use the shell's own transparency: trace modes, verbose output, and strategic echo statements. With the right techniques, tracking down a bug takes minutes, not hours.
set -x — Execution Trace
#!/usr/bin/env bash
set -x # enable trace
name="Alice"
echo "Hello, $name"
# Output:
# + name=Alice
# + echo 'Hello, Alice'
# Hello, Alice
# Enable/disable for just a section
set +x
sensitive_operation
set -xCustomising PS4 for Better Trace Output
PS4 is the prompt prefix for set -x trace lines (default is +). You can make it show the script name, line number, and function name:
#!/usr/bin/env bash
# Rich PS4: shows script, line, function
export PS4='+(${BASH_SOURCE##*/}:${LINENO})[${FUNCNAME[0]:-main}]: '
set -x
function greet {
local name="$1"
echo "Hello, $name"
}
greet "Alice"
# Trace output becomes:
# +(script.sh:8)[main]: greet Alice
# +(script.sh:4)[greet]: local name=Alice
# +(script.sh:5)[greet]: echo 'Hello, Alice'Running a Script in Debug Mode
# Without modifying the script:
bash -x script.sh
# Check syntax without running:
bash -n script.sh
# Both:
bash -nx script.sh
# Trace only one line with subshell
(set -x; some_command) # trace ends when () subshell exitsCommon Bugs and How to Find Them
Top 5 Bash Bug Patterns
- Quoting: Unquoted
$varsplits on spaces. Always quote. - Spaces around =:
var = valuerunsvaras a command. - [ vs [[: Using
==inside[ ]or>/<without escaping. - Subshell scope: Setting variables in
$(), pipes, or( )— changes not visible outside. - Integer comparison with strings:
[ "10" > "9" ]is lexicographic — use[[ 10 -gt 9 ]]or(( 10 > 9 )).
# Debug print helper — shows variable name and value
function dbg {
printf '[DEBUG %s:%s] %s=%q
' "${BASH_SOURCE[1]##*/}" "$BASH_LINENO" "$1" "${!1}" >&2
}
name="Alice Smith"
count=42
dbg name # [DEBUG script.sh:8] name='Alice Smith'
dbg count # [DEBUG script.sh:9] count=42
# Pause and inspect (interactive debugging)
function breakpoint {
echo "=== BREAKPOINT ===" >&2
echo "Line: $BASH_LINENO" >&2
read -rp "Press Enter to continue (or Ctrl+C to abort)..." >&2
}Using ShellCheck
ShellCheck — static analysis for Bash
ShellCheck is the most useful Bash tool after the shell itself. It catches:
- Unquoted variables that could split
- Incorrect
[vs[[usage - Pipes to
while(subshell scope loss) - Incorrect
return/exitusage - Dozens of other common mistakes
Install: brew install shellcheck or apt install shellcheck
Run: shellcheck script.sh
VS Code: install the ShellCheck extension for inline warnings.
What does `$BASH_LINENO` contain inside a function?
This script has multiple bugs — find and fix them all:
#!/usr/bin/env bash
name = "Alice"
files=file1.txt file2.txt file3.txt
for f in $files; do
if [ -f $f ]; then
count=$(wc -l $f)
echo $name has $count lines in $f
fi
done
Hint: there are at least 4 bugs.