Table of Contents
- Why Build a Custom Bash Debugger?
- Prerequisites
- Understanding Bash Debugging Fundamentals
- Designing Our Debugger: Core Features
- Step 1: Parsing the Target Script
- Step 2: Implementing Breakpoints
- Step 3: Adding Step-by-Step Execution
- Step 4: Building a Debugger Prompt
- Step 5: Testing the Debugger with a Sample Script
- Enhancing the Debugger: Advanced Features
- Conclusion
- References
Why Build a Custom Bash Debugger?
Bash’s built-in debugging tools are limited:
set -xprints every command before execution (verbose, no control).set -eexits on error (but doesn’t help diagnose why).
A custom debugger lets you:
- Pause execution at specific lines (breakpoints).
- Inspect variables at runtime.
- Step through code line-by-line.
- Add custom logic (e.g., conditional breakpoints).
Plus, building one teaches you Bash internals: signal handling, command evaluation, and script parsing.
Prerequisites
- Basic Bash knowledge (variables, loops, functions).
- A Unix-like environment (Linux/macOS).
- Bash 4.0+ (for associative arrays; check with
bash --version).
Understanding Bash Debugging Fundamentals
Before building, let’s recap key Bash concepts:
- Script Execution: Bash reads scripts line-by-line, executing each command in sequence.
- Signal Handling: Traps (
trap) can catch signals (e.g.,SIGINT) to pause/resume execution. - Command Evaluation:
eval "command"executes a string as a Bash command. - Associative Arrays:
declare -A breakpointsstores key-value pairs (e.g.,line_number=true).
Designing Our Debugger: Core Features
Our minimal debugger will support:
- Breakpoints: Pause at specified line numbers.
- Step Execution: Run one line at a time.
- Variable Inspection: Print values of variables.
- Interactive Prompt: Accept commands like
next,continue,print,quit.
Step 1: Parsing the Target Script
First, the debugger needs to read and parse the target script. We’ll store lines in an array, skipping comments and empty lines (to avoid debugging noise).
Code Snippet: Parsing the Script
#!/bin/bash
# Debugger variables
declare -a script_lines # Stores non-empty, non-comment lines
declare -i current_line=0 # Tracks current execution line
declare -A breakpoints # Associative array: key=line_number, value=true
# Parse target script into script_lines array
parse_script() {
local target_script="$1"
local line_num=0
# Read script line-by-line
while IFS= read -r line; do
((line_num++))
# Skip empty lines and comments (starting with #)
if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then
continue
fi
# Store line with original line number (for breakpoints)
script_lines+=("$line_num:$line")
done < "$target_script"
}
Explanation:
parse_scriptreads the target script, skips comments/empty lines, and stores lines inscript_lineswith their original line numbers (e.g.,5:echo "Hello").script_linesusesline_num:lineto track original line numbers (critical for breakpoints).
Step 2: Implementing Breakpoints
Breakpoints let users pause execution at specific lines. We’ll let users set breakpoints via the break command (e.g., break 10).
Code Snippet: Breakpoint Management
# Add a breakpoint
add_breakpoint() {
local line_num="$1"
breakpoints["$line_num"]=true
echo "Breakpoint set at line $line_num"
}
# Check if current line has a breakpoint
has_breakpoint() {
local line_num="$1"
[[ -n "${breakpoints[$line_num]}" ]]
}
Explanation:
add_breakpointstores line numbers in thebreakpointsassociative array.has_breakpointchecks if the current line is inbreakpoints.
Step 3: Adding Step-by-Step Execution
Next, we need to run the script line-by-line. We’ll loop through script_lines, execute each line, and pause if:
- The line is a breakpoint.
- The user requested
step(single-line execution).
Code Snippet: Execution Loop
execute_script() {
local target_script="$1"
parse_script "$target_script"
# Main execution loop
for entry in "${script_lines[@]}"; do
# Split entry into original line number and line content (e.g., "5:echo ...")
local line_num="${entry%%:*}"
local line_content="${entry#*:}"
((current_line++))
# Pause if breakpoint or step mode is active
if has_breakpoint "$line_num" || [[ "$step_mode" == true ]]; then
echo -e "\n[PAUSED] Line $line_num: $line_content"
interactive_prompt
fi
# Execute the line
echo -e "\nExecuting: $line_content"
eval "$line_content" # Execute the line as a Bash command
local exit_code=$?
# Handle errors
if [[ $exit_code -ne 0 ]]; then
echo "Error: Line $line_num exited with code $exit_code"
exit $exit_code
fi
done
}
Explanation:
execute_scriptloops throughscript_lines, splits each entry intoline_num(original line) andline_content(code to run).- Before executing, it checks for breakpoints or
step_mode(enabled viastepcommand) and pauses. eval "$line_content"executes the line. We capture the exit code to handle errors.
Step 4: Building a Debugger Prompt
When paused, the debugger needs an interactive prompt to accept user commands.
Code Snippet: Interactive Prompt
# Interactive prompt for user commands
interactive_prompt() {
local command
while true; do
read -p "(debugger) " command
case "$command" in
next|n) # Step to next line
step_mode=true
break
;;
continue|c) # Resume until next breakpoint
step_mode=false
break
;;
print|p) # Print variable (usage: print var)
local var="${command#* }"
if [[ -z "$var" ]]; then
echo "Usage: print <variable>"
continue
fi
echo "$var=${!var}" # Indirect expansion to get variable value
;;
list|l) # List surrounding lines
local start=$((current_line - 2))
local end=$((current_line + 2))
echo "Lines $start-$end:"
for ((i=start; i<=end; i++)); do
if [[ -n "${script_lines[$i-1]}" ]]; then
echo " ${script_lines[$i-1]}"
fi
done
;;
quit|q) # Exit debugger
echo "Exiting debugger."
exit 0
;;
*)
echo "Unknown command: $command. Use: next (n), continue (c), print (p), list (l), quit (q)"
;;
esac
done
}
Explanation:
interactive_promptreads user input and handles commands:next/n: Steps to the next line (enablesstep_mode).continue/c: Resumes execution until the next breakpoint.print/p var: Prints the value ofvar(uses indirect expansion${!var}).list/l: Shows 2 lines before/after the current line.quit/q: Exits the debugger.
Step 5: Testing the Debugger with a Sample Script
Let’s test our debugger with a buggy script. Create sample_script.sh:
#!/bin/bash
# A script with a logical error
count=0
for i in {1..3}; do
count=$((count + 1)) # Correct: should increment by 1 each loop
# Bug: The next line mistakenly sets count to 10 instead of adding
count=10
done
echo "Final count: $count" # Expected: 3, Actual: 10
Goal: Use the debugger to find why count is 10 instead of 3.
Running the Debugger
-
Save the debugger code as
bash_debugger.shand make it executable:chmod +x bash_debugger.sh -
Run the debugger with the sample script:
./bash_debugger.sh sample_script.sh -
Set a breakpoint at line 5 (the loop body):
(debugger) break 5 -
Use
continueto run to the breakpoint, thennextto step through lines. Useprint countto inspect the variable:[PAUSED] Line 5: count=$((count + 1)) (debugger) print count count=0 (debugger) next Executing: count=$((count + 1)) [PAUSED] Line 6: count=10 (debugger) print count count=1 (debugger) next Executing: count=10 [PAUSED] Line 5: count=$((count + 1)) (debugger) print count count=10 # Oh! Line 6 overwrites count to 10. Found the bug!
Enhancing the Debugger: Advanced Features
To make the debugger more powerful, add:
1. Conditional Breakpoints
Pause only if a condition is met (e.g., break 5 if $count > 5).
2. Watchpoints
Break when a variable changes (e.g., watch count).
3. Backtraces
Show the call stack (useful for debugging functions).
Example: Conditional Breakpoints
# Add to add_breakpoint function
add_breakpoint() {
local line_num="$1"
local condition="${2:-true}" # Default: always break
breakpoints["$line_num"]="$condition" # Store condition instead of "true"
echo "Breakpoint set at line $line_num (condition: $condition)"
}
# Modify has_breakpoint to check condition
has_breakpoint() {
local line_num="$1"
local condition="${breakpoints[$line_num]}"
[[ -n "$condition" ]] && eval "$condition" # Evaluate condition
}
Now users can set: break 5 if $count -gt 2 (pause line 5 only if count > 2).
Conclusion
You’ve built a functional Bash script debugger! This tool helps you pause execution, inspect variables, and step through code—skills that translate to debugging real-world scripts.
Key takeaways:
- Bash’s
evaland associative arrays enable dynamic execution and state management. - Interactive prompts turn static scripts into interactive tools.
- Parsing and filtering script lines is critical for focused debugging.
To expand further, explore bashdb (a full-featured Bash debugger) or add watchpoints/backtraces to your custom tool.
References
- Bash Manual (official docs for
eval, traps, arrays). - Bashdb (advanced Bash debugger).
- Shell Scripting Tutorial (Bash basics).
- Bash Associative Arrays.
Happy debugging! 🐞