Bash Debugging
Techniques and tools for debugging Bash scripts
🐛 What is Bash Debugging?
Bash debugging helps you find and fix errors in your scripts. Using built-in debugging options and techniques, you can trace execution, inspect variables, and identify problems quickly to create reliable scripts.
#!/bin/bash -x # Enable debug mode
name="John"
echo "Hello, $name"
Output:
+ name=John + echo 'Hello, John' Hello, John
Key Debugging Techniques
set -x
Print commands before execution
set -x
echo "Debug mode on"
set -e
Exit on any error
set -e
command_that_fails
set -v
Print input lines as read
set -v
echo "Verbose mode"
set -u
Error on undefined variables
set -u
echo $undefined_var
🔹 Debug Mode (set -x)
The set -x command activates Bash debug mode, printing each command before execution with expanded variables. This transparent debugging technique reveals exactly what your script executes, making variable expansions and command substitutions visible. By tracing execution step-by-step, developers can quickly identify syntax errors, incorrect variable values, and logical flaws. Debug mode is essential for troubleshooting complex scripts and understanding execution flow, providing immediate visibility into what would otherwise be hidden operations.
#!/bin/bash
# Enable debug mode
set -x
name="Alice"
age=25
echo "Name: $name, Age: $age"
# Disable debug mode
set +x
echo "Debug mode off"
Output:
+ name=Alice + age=25 + echo 'Name: Alice, Age: 25' Name: Alice, Age: 25 + set +x Debug mode off
🔹 Exit on Error (set -e)
Using set -e ensures your script terminates immediately when any command returns a non-zero exit status. This prevents cascading failures where subsequent commands execute despite earlier errors, potentially causing data corruption or incorrect results. For production scripts, this is a critical safety measure that stops execution at the first sign of trouble. Combined with proper error handling, it creates more reliable automation that fails fast rather than continuing with potentially dangerous operations.
#!/bin/bash
set -e # Exit on error
echo "Step 1: Success"
ls /nonexistent/path # This fails
echo "Step 2: Won't execute"
Output:
Step 1: Success ls: cannot access '/nonexistent/path': No such file or directory
🔹 Undefined Variable Detection (set -u)
The set -u option treats undefined variables as fatal errors rather than silently treating them as empty strings. This catches typos in variable names and missing assignments that could otherwise cause mysterious failures. When enabled, any reference to an unset variable immediately terminates the script with an error message. This significantly improves script reliability by ensuring all variables are properly initialized before use, eliminating a common source of bugs.
#!/bin/bash
set -u # Error on undefined variables
defined_var="Hello"
echo $defined_var # Works fine
echo $undefined_var # Causes error
Output:
Hello bash: undefined_var: unbound variable
🔹 Combining Debug Options
Combine multiple debugging options using set -euxo pipefail for comprehensive error detection and tracing. This powerful combination provides exit-on-error, undefined-variable detection, command tracing, and pipeline error handling simultaneously. The -o pipefail ensures pipelines fail if any component fails, not just the last command. This represents industry best practices for robust shell scripting, catching multiple error types while providing detailed execution visibility.
#!/bin/bash
# Strict mode: exit on error, undefined vars, and show commands
set -euxo pipefail
echo "Starting script..."
file_count=$(ls *.txt | wc -l)
echo "Found $file_count text files"
Output:
+ echo 'Starting script...' Starting script... ++ ls '*.txt' ++ wc -l + file_count=5 + echo 'Found 5 text files' Found 5 text files
🔹 Debug Functions
Create custom debug functions with conditional logging controlled by environment variables or command-line flags. These functions can include timestamps, function names, line numbers, and variable values while allowing easy enabling/disabling. During development, detailed debug output helps trace execution and identify issues, while production deployments can disable verbose logging. This approach maintains clean production output while preserving powerful debugging capabilities when needed.
#!/bin/bash
DEBUG=true
debug() {
if [ "$DEBUG" = true ]; then
echo "[DEBUG] $*" >&2
fi
}
debug "Script started"
name="Bob"
debug "Variable name set to: $name"
echo "Hello, $name"
debug "Script completed"
Output:
[DEBUG] Script started [DEBUG] Variable name set to: Bob Hello, Bob [DEBUG] Script completed
🔹 Checking Exit Codes
Every command returns an exit code where 0 indicates success and non-zero values represent different failure types. Proper exit code checking enables graceful error handling and decision-making in scripts. Use $? to access the previous command's exit status immediately after execution. Robust scripts check critical operations and respond appropriately to failures, whether through retries, alternative paths, or clean termination with informative error messages.
#!/bin/bash
# Check if file exists
if ls myfile.txt 2>/dev/null; then
echo "File exists"
else
echo "File not found (exit code: $?)"
fi
# Save and check exit code
grep "pattern" file.txt
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "Pattern found"
else
echo "Pattern not found"
fi
Output:
File not found (exit code: 2) Pattern not found
🔹 Using PS4 for Better Debug Output
Customize the PS4 variable to enhance debug output with line numbers, function names, timestamps, and process IDs. When using set -x, PS4 controls the prompt prefix for each debug line. Enhanced formatting like export PS4='+ [$(date) ${FUNCNAME[0]:-main}:${LINENO}] ' provides contextual information that makes tracing complex script execution much easier, especially in functions and loops where multiple operations occur.
#!/bin/bash
# Custom debug prompt with line numbers
export PS4='+ Line ${LINENO}: '
set -x
echo "First command"
echo "Second command"
result=$((5 + 3))
echo "Result: $result"
Output:
+ Line 7: echo 'First command' First command + Line 8: echo 'Second command' Second command + Line 9: result=$((5 + 3)) + Line 10: echo 'Result: 8' Result: 8