Debug Shell Scripts with bash -x: Complete Guide

Master shell script debugging with bash -x option. Learn trace execution, command options, and advanced techniques for system administrators and developers.

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.sh

Method 2: Add to shebang line

#!/bin/bash -x

Method 3: Enable within script

set -x

script content

set +x

Method 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.sh

Or 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 +x

echo "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 pipefail

Function 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"' ERR

Simulate some operations that might fail

echo "Starting operations..." mkdir -p /tmp/test_dir cd /tmp/test_dir

This might fail if file doesn't exist

cat non_existent_file.txt

echo "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.log

Or 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" done

set +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}): ' fi

Main 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 bashdb

Run script with debugger

bashdb script.sh

Common 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.log

Redirect stderr to fd 3 when debugging

[ "$DEBUG" -eq 1 ] && exec 2>&3

set -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 fi

critical_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.

Tags

  • DevOps
  • Linux
  • bash
  • debugging
  • shell-scripting

Related Articles

Popular Technical Articles & Tutorials

Explore our comprehensive collection of technical articles, programming tutorials, and IT guides written by industry experts:

Browse all 8+ technical articles | Read our IT blog

Debug Shell Scripts with bash -x: Complete Guide