Stage 2 · Tools
Bash Fundamentals
Debugging Scripts
set -x, set -eauo pipefail, shellcheck, and tracing for finding bugs fast.
Debug Modes
Bash provides several built-in debugging tools. The most useful are set -x for command tracing, set -v for showing source lines, and set -e for catching errors early. Combining these with external tools like ShellCheck gives you powerful debugging capabilities.
# Option 1: Enable at the top of the script
set -x # Command tracing
# Option 2: Enable for a section
set -x
# ... suspicious code ...
set +x # Disable tracing
# Option 3: Run the script with debug mode
bash -x script.sh
# Option 4: Enable via environment variable
BASH_XTRACEFD=2 bash -x script.sh 2>debug.logset -x prints each command to stderr before executing it. The output is prefixed with ++ for subshells, + for the main shell. BASH_XTRACEFD redirects trace output to a file descriptor.
set -x Tracing
set -x is the most commonly used debugging tool in Bash. It shows you exactly what the shell executes after variable expansion and word splitting.
#!/usr/bin/env bash
set -x
NAME="Alice"
GREETING="Hello, $NAME"
echo "$GREETING"
# Output:
# + NAME=Alice
# + GREETING='Hello, Alice'
# + echo 'Hello, Alice'
# Hello, AliceThe trace output is prefixed with + and shows the fully expanded command. Notice how variables are already substituted — this helps you see exactly what the shell executes.
PS4='+{BASH_SOURCE}:{LINENO}: ' makes trace output show the filename and line number. This is extremely helpful for large scripts where you need to know which function each trace line comes from.
ShellCheck
ShellCheck is a static analysis tool for shell scripts. It catches common mistakes, suggests improvements, and warns about portability issues. It is the single most useful tool for improving shell script quality.
# Install
# macOS
brew install shellcheck
# Ubuntu/Debian
sudo apt install shellcheck
# Run on a file
shellcheck script.sh
# Include severity levels
shellcheck -S warning script.sh
# Output in JSON format
shellcheck -f json script.sh
# Check with specific shell
shellcheck -s bash script.shShellCheck analyzes your script without running it. It finds bugs like unquoted variables, missing shebang, incorrect quoting, and non-POSIX constructs.
#!/usr/bin/env bash
# SC2086: Double quote to prevent globbing and word splitting
files=$(ls *.txt) # Warning: unquoted variable
# SC2046: Quote this to prevent word splitting
rm $(find /tmp -name "*.log") # Warning: unquoted command sub
# SC2034: Variable appears unused
unused_var="hello" # Warning: assigned but never used
# SC2162: read without -r will mangle backslashes
read user_input # Warning: use read -r insteadEach ShellCheck warning has a code (SC####). You can disable specific warnings with # shellcheck disable=SC2086 on the line before the issue.
ShellCheck catches bugs before they reach production. Add it as a pre-commit hook or CI step. Most issues it flags are real bugs or portability problems worth fixing.
Common Bugs and Fixes
Shell scripts have predictable failure modes. Learning these patterns helps you write defensive code and debug faster.
| Bug | Symptom | Fix |
|---|---|---|
| Unquoted variable | Word splitting, glob expansion | Always use "$VAR" |
| Missing shebang | Wrong shell, syntax errors | Always start with #!/usr/bin/env bash |
| Spaces around = | Command not found | Use VAR=value, no spaces |
| Missing set -e | Errors silently ignored | Always use set -euo pipefail |
| Unquoted $@ in for | Arguments split on spaces | Use while shift pattern instead |
#!/usr/bin/env bash
set -x # Enable tracing
process_files() {
local dir="$1"
# Bug: unquoted glob expands incorrectly with spaces
for file in $dir/*.txt; do
echo "Processing: $file"
# ... process ...
done
}
process_files "/tmp/my files" # Breaks on space in path
# Fix: quote the variable and use find for safety
process_files_safe() {
local dir="$1"
while IFS= read -r -d '' file; do
echo "Processing: $file"
done < <(find "$dir" -maxdepth 1 -name "*.txt" -print0)
}When debugging: enable set -x to see what your script actually executes. The trace shows variable expansion, revealing where word splitting or globbing causes problems.
Debugging Strategies
When a script fails, follow a systematic approach rather than randomly adding echo statements.
- Read the error message carefully — it tells you the line number and error type.
- Run with bash -x to see the exact commands executed before the failure.
- Add echo statements around the failing line to inspect variable values.
- Use set -x inside functions to trace specific code sections.
- Run ShellCheck to catch common mistakes you might have missed.
- Test with set -euo pipefail to ensure all errors are caught.
- Check file permissions, path validity, and command availability.
# Debug only a specific function
my_function() {
set +x # Disable if parent had it on
echo "=== DEBUG: my_function ===" >&2
set -x # Enable for the function body
# ... function logic ...
}
# Debug with context
debug_var() {
echo "[DEBUG] $1 = $(eval echo $$1)" >&2
}
NAME="test"
debug_var NAME # [DEBUG] NAME = testSometimes you need to debug a specific function without tracing the entire script. Disable tracing at the function entry and re-enable it for the code you want to inspect.
BASH_XTRACEFD=20 bash -x script.sh 20>trace.log sends trace output to a file instead of stderr. This keeps your terminal clean while still capturing full trace information for later analysis.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.