Table of Contents
- Common Bash Script Issues: What Goes Wrong?
- Built-in Bash Debugging Tools
- Manual Debugging Techniques
- Advanced Debugging Tools
- Best Practices to Avoid Bugs Proactively
- Real-World Debugging Example
- Conclusion
- References
Common Bash Script Issues: What Goes Wrong?
Before diving into debugging tools, it’s helpful to recognize the most frequent culprits behind Bash script failures. Awareness of these issues will help you spot problems faster:
1. Syntax Errors
Bash is unforgiving of syntax mistakes. Common examples include:
- Missing closing quotes or parentheses.
- Forgetting semicolons or
do/donein loops. - Mismatched braces (e.g.,
{without}).
Example:
# Missing closing quote causes a syntax error
echo "Hello World
2. Variable Expansion Quirks
Unquoted variables can lead to unexpected word splitting or globbing (e.g., * expanding to filenames).
Example:
filename="my file.txt"
# Unquoted $filename splits into "my" and "file.txt"
cat $filename # Error: "cat: my: No such file or directory"
3. Incorrect Loop Behavior
Looping over command output (e.g., ls *.txt) can break if filenames contain spaces or special characters.
Example:
# Bug: Splits "file with spaces.txt" into three arguments
for file in $(ls *.txt); do
echo "Processing $file"
done
4. Subshell Side Effects
Commands run in subshells (e.g., (cd /tmp; pwd)) don’t affect the parent shell, leading to unexpected state changes.
Example:
# Subshell cd doesn't change parent directory
(cd /tmp)
pwd # Still in original directory, not /tmp
5. Ignoring Exit Codes
Failing to check if a command succeeded (via $?) can cause scripts to proceed with invalid data.
Example:
# Script continues even if grep fails to find "error"
grep "error" log.txt
echo "Errors found!" # Falsely claims errors exist
Built-in Bash Debugging Tools
Bash includes built-in features to help diagnose issues without external tools. These are lightweight and always available.
The set Command: Your First Line of Defense
The set command modifies shell behavior. Adding debugging flags at the start of a script (or temporarily) reveals execution details.
| Flag | Purpose |
|---|---|
-x (xtrace) | Prints each command and its arguments before execution (most useful). |
-v (verbose) | Prints input lines as they are read (good for syntax issues). |
-e (errexit) | Exits immediately if any command fails (non-zero exit code). |
-u (nounset) | Treats undefined variables as errors (avoids $undefined pitfalls). |
-o pipefail | Makes a pipeline return the exit code of the last failed command (not just the last command). |
Example: Using set -x
Add set -x to the top of your script to trace execution:
#!/bin/bash
set -x # Enable debugging
filename="test.txt"
echo "Creating $filename"
touch "$filename"
if [ -f "$filename" ]; then
echo "$filename exists!"
fi
set +x # Disable debugging
Output:
+ filename=test.txt
+ echo 'Creating test.txt'
Creating test.txt
+ touch test.txt
+ [ -f test.txt ]
+ echo 'test.txt exists!'
test.txt exists!
+ set +x
The + prefix shows commands being executed, making it easy to spot where things go wrong.
Exit Codes: Understanding $?
Every command returns an exit code (0 = success, non-zero = failure). Use $? to check the last command’s exit code.
Example:
grep "error" log.txt
if [ $? -ne 0 ]; then # If grep failed (exit code != 0)
echo "No errors found."
fi
Pro Tip: Combine with set -e to auto-exit on failure, preventing cascading errors.
Trap Signals for Error Handling
The trap command catches signals (e.g., errors) and runs a custom command, helping you debug crashes.
Example: Log Errors on Exit
#!/bin/bash
set -e # Exit on error
# Trap errors and print debug info
trap 'echo "Error at line $LINENO: Command failed: $BASH_COMMAND"' ERR
cd /invalid/directory # This will fail, triggering the trap
echo "This line never runs."
Output:
Error at line 6: Command failed: cd /invalid/directory
Manual Debugging Techniques
For quick checks or when external tools aren’t available, manual techniques can isolate issues.
Echo and Printf: The Simplest Debuggers
Insert echo or printf statements to print variable values, loop indices, or execution steps.
Example:
filename="data.csv"
echo "Debug: filename = '$filename'" # Quotes reveal whitespace
if [ -f "$filename" ]; then
echo "Debug: File exists"
process_data "$filename"
else
echo "Debug: File missing"
fi
Inspecting Variables and Subshells
Use declare -p to print a variable’s type and value (handy for arrays or special variables).
Example:
files=("file1.txt" "file2.txt")
declare -p files # Prints: declare -a files=([0]="file1.txt" [1]="file2.txt")
To debug subshells, redirect output to a log file:
# Log subshell output to debug.log
(cd /tmp; ls -l) > debug.log 2>&1
Advanced Debugging Tools
For complex scripts, built-in tools may not be enough. These external utilities supercharge your debugging workflow.
ShellCheck: Catch Errors Before Execution
ShellCheck is a static analysis tool that flags syntax errors, bad practices, and portability issues. It’s like a linter for Bash scripts.
Installation:
- Ubuntu/Debian:
sudo apt install shellcheck - macOS:
brew install shellcheck - Windows: Use WSL or download from GitHub.
Usage: Run shellcheck script.sh on your script.
Example Output:
In script.sh line 3:
for file in $(ls *.txt); do
^------------^ SC2045: Iterating over ls output is fragile. Use a glob.
In script.sh line 4:
echo "Processing $file"
^-----^ SC2086: Double quote to prevent globbing and word splitting.
ShellCheck explains why the code is risky and suggests fixes (e.g., use for file in *.txt instead of ls).
Bashdb: The Bash Debugger
Bashdb is a command-line debugger for Bash, modeled after gdb (GNU Debugger). It lets you set breakpoints, step through code, and inspect variables interactively.
Installation:
- Ubuntu/Debian:
sudo apt install bashdb - macOS:
brew install bashdb
Basic Workflow:
- Start debugging:
bashdb script.sh - Set a breakpoint:
break 5(stop at line 5) - Run to breakpoint:
run - Step to next line:
next - Inspect a variable:
print filename - Continue execution:
continue
Example Session:
bashdb test.sh
bashdb<0> break 4 # Break at line 4
Breakpoint 1 set in file test.sh, line 4.
bashdb<0> run
Starting program: /bin/bash test.sh
Breakpoint 1, main() at test.sh:4
4 filename="data.txt"
bashdb<1> print filename # Variable is undefined yet
filename = ''
bashdb<1> next # Execute line 4
5 echo "Processing $filename"
bashdb<1> print filename
filename = 'data.txt'
bashdb<1> continue # Run rest of script
Processing data.txt
IDEs and Editor Integrations
Modern IDEs and editors simplify debugging with built-in or plugin-based support:
- VS Code: Install the ShellCheck and Bash Debug extensions for real-time linting and debugging.
- JetBrains (IntelliJ, PyCharm): Enable Bash support via plugins for syntax highlighting and debugging.
- Vim/Neovim: Use
ALE(Asynchronous Lint Engine) with ShellCheck integration.
Best Practices to Avoid Bugs Proactively
Preventing bugs is easier than fixing them. Adopt these habits to write more robust scripts:
1. Use set -euo pipefail
Start scripts with #!/bin/bash followed by set -euo pipefail to catch errors early:
-e: Exit on command failure.-u: Treat undefined variables as errors.-o pipefail: Fail if any command in a pipeline fails.
2. Quote Variables Always
Quoting ("$variable") prevents word splitting and globbing.
Bad: rm $temp_files (dangerous if temp_files contains *)
Good: rm "$temp_files"
3. Write Modular Functions
Break scripts into functions with clear inputs/outputs. This isolates bugs to specific code blocks.
Example:
process_file() {
local filename="$1" # Local variable to avoid side effects
if [ ! -f "$filename" ]; then
echo "Error: $filename not found" >&2
return 1
fi
# ... processing logic ...
}
4. Test Edge Cases
Validate inputs (e.g., empty files, special characters), and test with set -x to simulate real-world scenarios.
5. Comment and Document
Explain why code works, not just what it does. This helps you (and others) debug later.
Real-World Debugging Example
Let’s walk through fixing a broken script using the tools above.
The Broken Script
This script is supposed to back up .txt files to a backup/ directory, but it fails when filenames have spaces:
#!/bin/bash
# Buggy script: Fails with filenames containing spaces
mkdir -p backup
files=$(ls *.txt) # Risky: Splits filenames with spaces
for file in $files; do
cp "$file" "backup/$file"
echo "Backed up $file"
done
Problem: If ls *.txt returns file 1.txt, $files becomes "file" "1.txt", and the loop processes file and 1.txt as separate files (both missing).
Debugging Steps
-
Enable
set -xto see variable expansion:
Addset -xat the top. Run the script—output shows:+ files='file 1.txt' + for file in $files + cp file backup/file cp: cannot stat 'file': No such file or directory -
Run ShellCheck to identify the root cause:
shellcheck backup.sh In backup.sh line 5: files=$(ls *.txt) ^----------^ SC2045: Iterating over ls output is fragile. Use a glob. -
Fix the Loop by using a glob directly (no
ls):# Fixed: Loop over *.txt directly to handle spaces for file in *.txt; do cp "$file" "backup/$file" echo "Backed up $file" done -
Test with
set -euo pipefailto catch other issues (e.g., missingbackup/directory, but we already havemkdir -p backup).
The Corrected Script
#!/bin/bash
set -euo pipefail # Exit on errors, undefined variables, or pipeline failures
mkdir -p backup
# Safe: Loop over *.txt glob (handles spaces in filenames)
for file in *.txt; do
# Skip if no .txt files exist (prevents "*.txt" from being treated as a filename)
[ -e "$file" ] || continue
cp "$file" "backup/$file"
echo "Backed up '$file'" # Quotes show spaces in output
done
Conclusion
Debugging Bash scripts is a skill that improves with practice. By understanding common pitfalls, leveraging built-in tools like set -x, and using advanced utilities like ShellCheck and Bashdb, you can diagnose issues faster. Proactive habits—quoting variables, writing modular code, and testing edge cases—will reduce bugs in the first place.
Remember: The goal isn’t just to fix scripts, but to write resilient ones. With the right tools and mindset, you’ll turn debugging from a chore into a systematic process.
References
- Bash Manual
- ShellCheck Documentation
- Bashdb User Guide
- Greg’s Wiki: BashFAQ (Common Bash questions/answers)
- “Learning the Bash Shell” by Cameron Newham (O’Reilly book)