Debug Shell Scripts with bash -x
Introduction
Shell script debugging is a critical skill for system administrators, developers, and DevOps engineers. The bash -x option is one of the most powerful and commonly used debugging techniques in shell scripting. It provides a trace execution mode that displays each command before it is executed, making it invaluable for identifying issues in complex scripts.
The -x option enables shell tracing, which prints each command to standard error (stderr) preceded by the value of the PS4 variable (typically + by default). This allows developers to see exactly what the shell is doing as it processes the script, including variable expansions, command substitutions, and arithmetic evaluations.
Understanding bash -x Option
Basic Syntax
The bash -x command can be used in several ways:
`bash
Method 1: Run script with -x option
bash -x script.shMethod 2: Add to shebang line
#!/bin/bash -xMethod 3: Enable within script
set -xscript content
set +xMethod 4: Enable for specific sections
set -x command1 command2 set +x`How It Works
When bash encounters the -x option, it enters trace mode where:
1. Each command is printed to stderr before execution 2. Variable expansions are shown in their expanded form 3. Command substitutions are displayed 4. Arithmetic expansions are revealed 5. Pathname expansions (globbing) are shown
Command Options and Variations
| Option | Description | Usage Example |
|--------|-------------|---------------|
| -x | Enable trace mode | bash -x script.sh |
| -v | Verbose mode - print input lines | bash -v script.sh |
| -n | No execute - syntax check only | bash -n script.sh |
| -e | Exit on error | bash -e script.sh |
| -u | Exit on undefined variable | bash -u script.sh |
| -o pipefail | Pipeline fails if any command fails | bash -o pipefail script.sh |
Combining Options
Multiple debugging options can be combined for comprehensive debugging:
`bash
Combine multiple options
bash -xveu script.shOr using long form
bash --xtrace --verbose --errexit --nounset script.sh`Practical Examples
Example 1: Basic Variable Debugging
`bash
#!/bin/bash
File: debug_example1.sh
name="John" age=25 city="New York"
echo "Hello, my name is $name" echo "I am $age years old" echo "I live in $city"
Calculate next year's age
next_year=$((age + 1)) echo "Next year I will be $next_year"`Running with bash -x:
`bash
$ bash -x debug_example1.sh
+ name=John
+ age=25
+ city='New York'
+ echo 'Hello, my name is John'
Hello, my name is John
+ echo 'I am 25 years old'
I am 25 years old
+ echo 'I live in New York'
I live in New York
+ next_year=26
+ echo 'Next year I will be 26'
Next year I will be 26
`
Example 2: Function Debugging
`bash
#!/bin/bash
File: debug_example2.sh
calculate_area() { local length=$1 local width=$2 local area=$((length * width)) echo "Area calculation: $length x $width = $area" return $area }
echo "Starting area calculation program"
calculate_area 10 5
result=$?
echo "Function returned: $result"
`
Debug output:
`bash
$ bash -x debug_example2.sh
+ echo 'Starting area calculation program'
Starting area calculation program
+ calculate_area 10 5
+ local length=10
+ local width=5
+ local area=50
+ echo 'Area calculation: 10 x 5 = 50'
Area calculation: 10 x 5 = 50
+ return 50
+ result=50
+ echo 'Function returned: 50'
Function returned: 50
`
Example 3: Conditional Logic Debugging
`bash
#!/bin/bash
File: debug_example3.sh
score=85
if [ $score -ge 90 ]; then grade="A" elif [ $score -ge 80 ]; then grade="B" elif [ $score -ge 70 ]; then grade="C" else grade="F" fi
echo "Score: $score, Grade: $grade"
Array processing
fruits=("apple" "banana" "cherry") for fruit in "${fruits[@]}"; do echo "Processing: $fruit" done`Debug output shows conditional flow:
`bash
$ bash -x debug_example3.sh
+ score=85
+ '[' 85 -ge 90 ']'
+ '[' 85 -ge 80 ']'
+ grade=B
+ echo 'Score: 85, Grade: B'
Score: 85, Grade: B
+ fruits=("apple" "banana" "cherry")
+ for fruit in "${fruits[@]}"
+ echo 'Processing: apple'
Processing: apple
+ for fruit in "${fruits[@]}"
+ echo 'Processing: banana'
Processing: banana
+ for fruit in "${fruits[@]}"
+ echo 'Processing: cherry'
Processing: cherry
`
Advanced Debugging Techniques
Custom PS4 Variable
The PS4 variable controls the prompt displayed before each traced command. You can customize it for more informative output:
`bash
#!/bin/bash
Enhanced debugging with custom PS4
Set custom PS4 for better debugging info
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'set -x
function process_data() { local data=$1 echo "Processing: $data" local result=$(echo "$data" | tr '[:lower:]' '[:upper:]') echo "Result: $result" }
process_data "hello world"
set +x
`
Enhanced debug output:
`bash
+(debug_enhanced.sh:12): process_data(): local data=hello world
+(debug_enhanced.sh:13): process_data(): echo 'Processing: hello world'
Processing: hello world
+(debug_enhanced.sh:14): process_data(): echo 'hello world'
+(debug_enhanced.sh:14): process_data(): tr '[:lower:]' '[:upper:]'
+(debug_enhanced.sh:14): process_data(): local result='HELLO WORLD'
+(debug_enhanced.sh:15): process_data(): echo 'Result: HELLO WORLD'
Result: HELLO WORLD
`
Selective Debugging
You can enable debugging for specific sections of your script:
`bash
#!/bin/bash
File: selective_debug.sh
echo "This line is not traced"
Enable debugging for critical section
set -x critical_variable="important_data" result=$(some_complex_command "$critical_variable") set +xecho "This line is also not traced"
Enable debugging with error handling
set -xe risky_operation set +xe`Error Handling and Debugging
Combining Error Handling with Debugging
`bash
#!/bin/bash
File: error_debug.sh
Enable strict error handling and debugging
set -euxo pipefailFunction to handle errors
error_handler() { echo "Error occurred in script at line $1" echo "Last command: $2" exit 1 }Set up error trap
trap 'error_handler ${LINENO} "$BASH_COMMAND"' ERRSimulate some operations that might fail
echo "Starting operations..." mkdir -p /tmp/test_dir cd /tmp/test_dirThis might fail if file doesn't exist
cat non_existent_file.txtecho "This line won't be reached if error occurs"
`
Debugging Complex Scripts
Example: File Processing Script
`bash
#!/bin/bash
File: file_processor.sh
process_log_files() { local log_dir=$1 local pattern=$2 if [ ! -d "$log_dir" ]; then echo "Error: Directory $log_dir does not exist" return 1 fi local count=0 for file in "$log_dir"/*.log; do if [ -f "$file" ]; then echo "Processing file: $file" local matches=$(grep -c "$pattern" "$file" 2>/dev/null || echo "0") echo "Found $matches matches in $(basename "$file")" ((count += matches)) fi done echo "Total matches found: $count" return 0 }
Main execution
main() { local log_directory="/var/log" local search_pattern="ERROR" echo "Starting log analysis..." process_log_files "$log_directory" "$search_pattern" if [ $? -eq 0 ]; then echo "Log processing completed successfully" else echo "Log processing failed" exit 1 fi }Enable debugging for main function only
set -x main "$@" set +x`Debugging Output Analysis
Understanding Trace Output
| Symbol | Meaning | Example |
|--------|---------|---------|
| + | Command being executed | + echo "hello" |
| ++ | Nested command (subshell) | ++ date +%Y |
| +++ | Deeper nesting level | +++ basename /path/file |
Reading Complex Traces
When debugging complex scripts, the trace output can become overwhelming. Here are strategies to manage it:
`bash
#!/bin/bash
File: complex_debug.sh
Redirect debug output to file
exec 2>debug.logOr redirect to both file and terminal
exec 2> >(tee debug.log)set -x
Your complex script here
for i in {1..100}; do result=$(complex_calculation "$i") process_result "$result" doneset +x
`
Performance Considerations
Debug Output Impact
| Aspect | Without -x | With -x | Impact | |--------|------------|---------|--------| | Execution Speed | Normal | 10-50% slower | Moderate | | Output Volume | Minimal | Very High | Significant | | Resource Usage | Normal | Higher I/O | Moderate | | Log File Size | Small | Large | Significant |
Optimization Strategies
`bash
#!/bin/bash
Conditional debugging based on environment
DEBUG=${DEBUG:-0}
if [ "$DEBUG" -eq 1 ]; then set -x export PS4='+(${BASH_SOURCE}:${LINENO}): ' fi
Your script content here
echo "Script running with DEBUG=$DEBUG"Conditional debug sections
debug_section() { [ "$DEBUG" -eq 1 ] && set -x # Complex operations here local result=$(heavy_computation) [ "$DEBUG" -eq 1 ] && set +x echo "$result" }`Common Debugging Scenarios
Scenario 1: Variable Assignment Issues
`bash
#!/bin/bash
Problem: Variables not being set correctly
Before debugging
user_input=" john doe " cleaned_input=$(echo $user_input | tr -d ' ') # Wrong approach echo "Cleaned: '$cleaned_input'"With debugging enabled
set -x user_input=" john doe " cleaned_input=$(echo "$user_input" | sed 's/^[[:space:]]//;s/[[:space:]]$//') echo "Cleaned: '$cleaned_input'" set +x`Scenario 2: Loop and Array Issues
`bash
#!/bin/bash
Problem: Array processing not working as expected
declare -a servers=("web1" "web2" "db1") declare -a results=()
set -x for server in "${servers[@]}"; do echo "Checking server: $server" status=$(ping -c 1 "$server" >/dev/null 2>&1 && echo "UP" || echo "DOWN") results+=("$server:$status") done
echo "Results: ${results[@]}"
set +x
`
Scenario 3: File Operation Debugging
`bash
#!/bin/bash
Problem: File operations failing silently
backup_files() { local source_dir=$1 local backup_dir=$2 set -x # Check if source exists [ ! -d "$source_dir" ] && { echo "Source directory does not exist: $source_dir" return 1 } # Create backup directory mkdir -p "$backup_dir" # Copy files for file in "$source_dir"/*; do if [ -f "$file" ]; then cp "$file" "$backup_dir/" echo "Backed up: $(basename "$file")" fi done set +x }
backup_files "/home/user/documents" "/backup/documents"
`
Best Practices
When to Use bash -x
| Use Case | Recommended | Reason | |----------|-------------|--------| | Development | Yes | Catch issues early | | Testing | Yes | Verify script behavior | | Production | Conditional | Performance impact | | Debugging Issues | Yes | Essential for troubleshooting | | Performance Critical | No | Adds overhead |
Script Organization for Debugging
`bash
#!/bin/bash
File: well_organized_debug.sh
Configuration section
readonly SCRIPT_NAME=$(basename "$0") readonly LOG_FILE="/tmp/${SCRIPT_NAME}.log" DEBUG=${DEBUG:-0}Logging functions
log_debug() { [ "$DEBUG" -eq 1 ] && echo "[DEBUG] $*" >&2 }log_info() { echo "[INFO] $*" >&2 }
log_error() { echo "[ERROR] $*" >&2 }
Enable debugging if requested
if [ "$DEBUG" -eq 1 ]; then set -x export PS4='+(${FUNCNAME[1]:-main}:${LINENO}): ' fiMain script logic
main() { log_info "Starting $SCRIPT_NAME" # Your main logic here local result=$(process_data) log_info "Processing completed with result: $result" }Execute main function
main "$@"`Alternative Debugging Methods
Using bashdb Debugger
`bash
Install bashdb (on Ubuntu/Debian)
sudo apt-get install bashdbRun script with debugger
bashdb script.shCommon bashdb commands:
n - next line
s - step into
c - continue
l - list source
p variable - print variable
b line - set breakpoint
`Logging-Based Debugging
`bash
#!/bin/bash
Alternative to -x: Custom logging
LOG_LEVEL=${LOG_LEVEL:-INFO}
log() { local level=$1 shift echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >&2 }
debug() { [ "$LOG_LEVEL" = "DEBUG" ] && log DEBUG "$@" }
Usage
debug "Starting function with parameters: $*" result=$(some_command) debug "Command result: $result"`Troubleshooting Common Issues
Issue 1: Too Much Output
`bash
Solution: Filter debug output
bash -x script.sh 2>&1 | grep -E "(^\+|\+\+|Error|Failed)"Or redirect to file and analyze
bash -x script.sh 2>debug.log grep "specific_function" debug.log`Issue 2: Debug Output Interfering with Script Output
`bash
#!/bin/bash
Separate debug output from regular output
Redirect debug to file descriptor 3
exec 3>debug.logRedirect stderr to fd 3 when debugging
[ "$DEBUG" -eq 1 ] && exec 2>&3set -x
echo "This goes to stdout"
echo "This debug info goes to fd 3" >&2
set +x
`
Issue 3: Performance Impact
`bash
#!/bin/bash
Conditional debugging with minimal performance impact
debug_enabled() { [ "${DEBUG:-0}" -eq 1 ] }
Only enable debugging for specific sections
if debug_enabled; then set -x ficritical_function() { # Critical code here local result="processed" echo "$result" }
result=$(critical_function)
if debug_enabled; then
set +x
fi
`
Conclusion
The bash -x option is an indispensable tool for shell script debugging. It provides immediate visibility into script execution, variable expansions, and command flow. While it can generate verbose output and impact performance, its benefits for development and troubleshooting far outweigh these concerns.
Key takeaways for effective debugging with bash -x:
1. Use it during development to catch issues early 2. Customize PS4 for more informative trace output 3. Combine with other bash options for comprehensive error handling 4. Use selective debugging for large scripts 5. Consider performance impact in production environments 6. Implement conditional debugging based on environment variables 7. Redirect debug output appropriately to avoid interference
By mastering bash -x and understanding its various applications, you can significantly improve your shell scripting skills and reduce the time spent troubleshooting script issues. Remember to combine it with good script organization practices and other debugging techniques for the most effective development workflow.