Debugging and Testing
Error handling, debugging techniques, and testing strategies for robust Bash scripts.
Error Handling
Basic Error Handling
# Exit on error
set -e # Exit on any error
set -u # Exit on undefined variable
set -o pipefail # Exit on pipe failure
# Combine all error handling
set -euo pipefail
# Check command success
if command -v git > /dev/null 2>&1; then
echo "Git is installed"
else
echo "Git is not installed"
exit 1
fi
Error Checking
# Check exit status
if ping -c 1 google.com > /dev/null 2>&1; then
echo "Network is available"
else
echo "Network is down"
exit 1
fi
# Store exit status
ping -c 1 google.com > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "Ping successful"
fi
# Check file operations
if [ -f "config.txt" ]; then
source config.txt
else
echo "Error: config.txt not found"
exit 1
fi
Try-Catch Equivalent
#!/bin/bash
# try_catch.sh
# Function to handle errors
handle_error() {
echo "Error occurred in line $1"
echo "Command: $2"
echo "Exit code: $3"
exit 1
}
# Set error trap
trap 'handle_error $LINENO "$BASH_COMMAND" $?' ERR
# Your code here
risky_command
another_command
Debugging Techniques
Debug Mode
# Enable debug mode
set -x # Print commands before execution
set +x # Disable debug mode
# Debug specific sections
echo "Starting debug section"
set -x
command1
command2
set +x
echo "End debug section"
# Run script in debug mode
bash -x script.sh
Verbose Output
# Enable verbose mode
set -v # Print input lines
set +v # Disable verbose mode
# Combine debug and verbose
set -xv
# Run with verbose
bash -v script.sh
Custom Debug Function
#!/bin/bash
# debug_function.sh
DEBUG=1 # Set to 1 for debug mode
debug() {
if [ "$DEBUG" -eq 1 ]; then
echo "DEBUG: $*" >&2
fi
}
# Usage
debug "Starting script"
debug "Processing file: $filename"
debug "Current value: $counter"
Testing Strategies
Unit Testing
#!/bin/bash
# test_functions.sh
# Function to test
add_numbers() {
echo $(( $1 + $2 ))
}
# Test function
test_add_numbers() {
local result
result=$(add_numbers 2 3)
if [ "$result" -eq 5 ]; then
echo "PASS: add_numbers(2, 3) = 5"
else
echo "FAIL: add_numbers(2, 3) = $result, expected 5"
return 1
fi
}
# Run tests
test_add_numbers
Test Framework
#!/bin/bash
# test_framework.sh
# Test counters
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
# Assert function
assert_equals() {
local expected="$1"
local actual="$2"
local message="$3"
TESTS_RUN=$((TESTS_RUN + 1))
if [ "$expected" = "$actual" ]; then
echo "PASS: $message"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo "FAIL: $message"
echo " Expected: $expected"
echo " Actual: $actual"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
}
# Test report
test_report() {
echo "===================="
echo "Test Results:"
echo " Tests run: $TESTS_RUN"
echo " Tests passed: $TESTS_PASSED"
echo " Tests failed: $TESTS_FAILED"
echo "===================="
if [ $TESTS_FAILED -eq 0 ]; then
echo "ALL TESTS PASSED"
exit 0
else
echo "SOME TESTS FAILED"
exit 1
fi
}
# Example usage
assert_equals "5" "$(add_numbers 2 3)" "Addition test"
assert_equals "hello" "$(echo hello)" "Echo test"
test_report
Integration Testing
#!/bin/bash
# integration_test.sh
# Setup test environment
setup_test() {
TEST_DIR="/tmp/test_$$"
mkdir -p "$TEST_DIR"
cd "$TEST_DIR"
echo "Test setup complete"
}
# Cleanup test environment
cleanup_test() {
cd /
rm -rf "$TEST_DIR"
echo "Test cleanup complete"
}
# Trap cleanup on exit
trap cleanup_test EXIT
# Run integration tests
run_integration_tests() {
setup_test
# Test file operations
echo "test content" > test.txt
if [ -f "test.txt" ]; then
echo "PASS: File creation"
else
echo "FAIL: File creation"
exit 1
fi
# Test command execution
if grep -q "test" test.txt; then
echo "PASS: File content"
else
echo "FAIL: File content"
exit 1
fi
}
run_integration_tests
Error Logging
Logging Functions
#!/bin/bash
# logging.sh
LOG_FILE="/var/log/script.log"
LOG_LEVEL="INFO" # DEBUG, INFO, WARN, ERROR
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
# Also output to stderr for errors
if [ "$level" = "ERROR" ]; then
echo "[$timestamp] [$level] $message" >&2
fi
}
# Logging functions
log_debug() { log "DEBUG" "$@"; }
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
# Usage
log_info "Script started"
log_warn "This is a warning"
log_error "This is an error"
Error Tracking
#!/bin/bash
# error_tracking.sh
ERROR_COUNT=0
ERROR_LOG="/tmp/errors.log"
# Function to log errors
log_error() {
local error_msg="$1"
local line_number="$2"
local command="$3"
ERROR_COUNT=$((ERROR_COUNT + 1))
{
echo "Error #$ERROR_COUNT"
echo "Time: $(date)"
echo "Line: $line_number"
echo "Command: $command"
echo "Message: $error_msg"
echo "---"
} >> "$ERROR_LOG"
}
# Error handler
error_handler() {
log_error "Command failed" "$1" "$2"
echo "Error logged to $ERROR_LOG"
}
# Set trap
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
# Your code here
Debugging Tools
Process Debugging
# Monitor script execution
strace -e trace=file bash script.sh
# Monitor system calls
strace -o trace.out bash script.sh
# Monitor file operations
strace -e trace=open,read,write bash script.sh
# Debug with gdb (for bash itself)
gdb bash
Variable Inspection
#!/bin/bash
# variable_debug.sh
# Function to dump variables
dump_vars() {
echo "=== Variable Dump ==="
set | grep "^[A-Z_][A-Z0-9_]*="
echo "===================="
}
# Function to inspect specific variables
inspect_var() {
local var_name="$1"
echo "Variable: $var_name"
echo "Value: ${!var_name}"
echo "Type: $(declare -p $var_name 2>/dev/null | cut -d' ' -f2)"
}
# Usage
MY_VAR="test value"
dump_vars
inspect_var "MY_VAR"
Performance Profiling
#!/bin/bash
# performance_profile.sh
# Time execution
time_execution() {
local start_time=$(date +%s.%N)
"$@"
local end_time=$(date +%s.%N)
local duration=$(echo "$end_time - $start_time" | bc)
echo "Execution time: ${duration}s"
}
# Memory usage
check_memory() {
local pid=$$
ps -o pid,vsz,rss,comm -p $pid
}
# Usage
time_execution sleep 1
check_memory
Common Debug Patterns
Debugging Conditionals
#!/bin/bash
# debug_conditionals.sh
debug_condition() {
local condition="$1"
local description="$2"
echo "Testing condition: $description"
echo "Expression: $condition"
if eval "$condition"; then
echo "Result: TRUE"
else
echo "Result: FALSE"
fi
echo "---"
}
# Usage
FILE="test.txt"
debug_condition "[ -f '$FILE' ]" "File exists check"
debug_condition "[ -z '$EMPTY_VAR' ]" "Empty variable check"
Loop Debugging
#!/bin/bash
# debug_loops.sh
debug_loop() {
local counter=0
for item in "$@"; do
counter=$((counter + 1))
echo "Iteration $counter: Processing '$item'"
# Your loop logic here
echo " Status: OK"
done
echo "Loop completed. Total iterations: $counter"
}
# Usage
debug_loop "file1.txt" "file2.txt" "file3.txt"
Testing Best Practices
Test Organization
#!/bin/bash
# test_organization.sh
# Test categories
run_unit_tests() {
echo "Running unit tests..."
# Unit test functions here
}
run_integration_tests() {
echo "Running integration tests..."
# Integration test functions here
}
run_system_tests() {
echo "Running system tests..."
# System test functions here
}
# Main test runner
main() {
echo "Starting test suite..."
run_unit_tests
run_integration_tests
run_system_tests
echo "Test suite completed."
}
main "$@"
Mock Functions
#!/bin/bash
# mock_functions.sh
# Original function
api_call() {
curl -s "https://api.example.com/data"
}
# Mock function for testing
mock_api_call() {
echo '{"status": "success", "data": "mock data"}'
}
# Enable mocking
enable_mocking() {
# Replace function with mock
eval "$(declare -f mock_api_call | sed 's/mock_api_call/api_call/')"
}
# Test with mocking
test_with_mock() {
enable_mocking
result=$(api_call)
echo "Mock result: $result"
}
Error Recovery
Retry Logic
#!/bin/bash
# retry_logic.sh
retry_command() {
local max_attempts="$1"
local delay="$2"
shift 2
local command="$@"
local attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt of $max_attempts..."
if eval "$command"; then
echo "Command succeeded on attempt $attempt"
return 0
else
echo "Command failed on attempt $attempt"
if [ $attempt -lt $max_attempts ]; then
echo "Retrying in ${delay}s..."
sleep $delay
fi
fi
attempt=$((attempt + 1))
done
echo "Command failed after $max_attempts attempts"
return 1
}
# Usage
retry_command 3 5 "ping -c 1 google.com"
Graceful Degradation
#!/bin/bash
# graceful_degradation.sh
# Try primary method, fallback to secondary
process_data() {
local input="$1"
# Try primary method
if command -v jq > /dev/null 2>&1; then
echo "Using jq for JSON processing"
echo "$input" | jq '.'
# Fallback to basic parsing
else
echo "jq not available, using basic parsing"
echo "$input" | sed 's/[{}"]//g'
fi
}
# Usage
JSON_DATA='{"name": "test", "value": 123}'
process_data "$JSON_DATA"
Debugging Checklist
Pre-Debug Checklist
- Check syntax:
bash -n script.sh - Check permissions:
ls -la script.sh - Check shebang: Verify
#!/bin/bash - Check dependencies: Verify required commands exist
- Check input data: Validate input parameters
Debug Process
- Enable debug mode:
set -x - Add logging: Insert debug statements
- Check variables: Verify variable values
- Test conditions: Verify conditional logic
- Check error handling: Ensure proper error handling
Post-Debug Checklist
- Remove debug statements: Clean up temporary code
- Test edge cases: Verify error conditions
- Performance check: Ensure no performance regression
- Documentation: Update comments and documentation
- Version control: Commit tested changes
Testing Automation
Continuous Testing
#!/bin/bash
# continuous_testing.sh
# Watch for file changes and run tests
watch_and_test() {
while true; do
if [ "$(find . -name '*.sh' -newer .last_test 2>/dev/null)" ]; then
echo "Changes detected, running tests..."
if ./run_tests.sh; then
echo "Tests passed"
else
echo "Tests failed"
fi
touch .last_test
fi
sleep 5
done
}
# Initialize
touch .last_test
watch_and_test
Test Coverage
#!/bin/bash
# test_coverage.sh
# Track function coverage
COVERED_FUNCTIONS=()
ALL_FUNCTIONS=()
# Mark function as covered
mark_covered() {
local func_name="$1"
COVERED_FUNCTIONS+=("$func_name")
}
# Calculate coverage
calculate_coverage() {
local total=${#ALL_FUNCTIONS[@]}
local covered=${#COVERED_FUNCTIONS[@]}
local coverage=$((covered * 100 / total))
echo "Coverage: $coverage% ($covered/$total functions)"
}
# Usage in tests
test_function() {
mark_covered "my_function"
# Test logic here
}
See Advanced Features for complex debugging scenarios and Performance Optimization for performance testing techniques.