Skip to main content

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

  1. Check syntax: bash -n script.sh
  2. Check permissions: ls -la script.sh
  3. Check shebang: Verify #!/bin/bash
  4. Check dependencies: Verify required commands exist
  5. Check input data: Validate input parameters

Debug Process

  1. Enable debug mode: set -x
  2. Add logging: Insert debug statements
  3. Check variables: Verify variable values
  4. Test conditions: Verify conditional logic
  5. Check error handling: Ensure proper error handling

Post-Debug Checklist

  1. Remove debug statements: Clean up temporary code
  2. Test edge cases: Verify error conditions
  3. Performance check: Ensure no performance regression
  4. Documentation: Update comments and documentation
  5. 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.