Comprehensive Reference Guide — set options, trap ERR, exit codes, debugging techniques, and defensive scripting
Proper error handling and debugging are essential for writing reliable Bash scripts. This includes using set options to control behavior, trapping errors, validating inputs, and using debugging tools to find and fix problems.
Exit Codes: Every command returns 0 for success, non-zero for failure
Set Options: Control script behavior with set -e, set -u, etc.
Traps: Catch signals and errors to perform cleanup or logging
Debugging: Use set -x, PS4, and other tools to trace execution
| Option | Long Form | Description |
|---|---|---|
set -e |
set -o errexit |
Exit on first error |
set -u |
set -o nounset |
Error on undefined variables |
set -o pipefail |
- | Pipeline fails if any command fails |
set -x |
set -o xtrace |
Print commands before execution |
set -v |
set -o verbose |
Print lines as read |
set -n |
set -o noexec |
Check syntax without running |
set -E |
set -o errtrace |
ERR trap inherited by functions |
set -T |
set -o functrace |
DEBUG trap inherited by functions |
set -euo pipefail is commonly called "strict mode" and catches most common errors.
| Code | Meaning | Example |
|---|---|---|
| 0 | Success | Command completed successfully |
| 1 | General error | Miscellaneous errors |
| 2 | Misuse of shell command | Invalid arguments |
| 126 | Command not executable | Permission denied |
| 127 | Command not found | Binary doesn't exist in PATH |
| 128+N | Killed by signal N | 130 = Ctrl+C (SIGINT) |
Checking command exit status and handling errors.
#!/bin/bash
# Basic error checking
echo "=== Check Exit Status ==="
# $? contains last exit status
ls /tmp > /dev/null
echo "ls /tmp: $?"
ls /nonexistent 2>/dev/null
echo "ls /nonexistent: $?"
echo ""
echo "=== If Statement Check ==="
if ls /tmp > /dev/null 2>&1; then
echo "ls succeeded"
else
echo "ls failed"
fi
if ls /nonexistent > /dev/null 2>&1; then
echo "ls succeeded"
else
echo "ls failed"
fi
echo ""
echo "=== Short-circuit Operators ==="
# && runs second command if first succeeds
ls /tmp > /dev/null && echo "Directory exists"
# || runs second command if first fails
ls /nonexistent 2>/dev/null || echo "Directory missing"
# Combined pattern
ls /nonexistent 2>/dev/null && echo "Found" || echo "Not found"
echo ""
echo "=== Check Specific Commands ==="
# Check if command exists
if command -v git > /dev/null 2>&1; then
echo "git is installed"
else
echo "git is not installed"
fi
# Check if file is readable
file="/etc/passwd"
if [[ -r "$file" ]]; then
echo "$file is readable"
fi
echo ""
echo "=== Function Return Values ==="
validate_number() {
local num=$1
if [[ "$num" =~ ^[0-9]+$ ]]; then
return 0 # Success
else
return 1 # Failure
fi
}
if validate_number "42"; then
echo "42 is valid"
fi
if ! validate_number "abc"; then
echo "abc is not a number"
fi
echo ""
echo "=== Die Function Pattern ==="
die() {
echo "ERROR: $1" >&2
exit "${2:-1}"
}
# Usage examples (commented to not exit script)
# [[ -f "$config" ]] || die "Config not found: $config"
# command -v docker &>/dev/null || die "Docker required" 2
echo "die() function defined for error handling"
echo ""
echo "=== Multiple Conditions ==="
check_prerequisites() {
local errors=0
if [[ ! -d "/tmp" ]]; then
echo "ERROR: /tmp missing" >&2
((errors++))
fi
if ! command -v bash &>/dev/null; then
echo "ERROR: bash required" >&2
((errors++))
fi
return $errors
}
if check_prerequisites; then
echo "All prerequisites met"
else
echo "Prerequisites check failed"
fi
Using set options for automatic error detection.
#!/bin/bash
# Strict mode demonstration
echo "=== set -e (errexit) ==="
# Exit on first error
(
set -e
echo "Before error"
false # This causes exit
echo "After error (never reached)"
) || echo "Subshell exited with error"
echo ""
echo "=== set -u (nounset) ==="
# Error on undefined variables
(
set -u
echo "Defined: ${HOME}"
# echo "Undefined: ${UNDEFINED_VAR}" # Would cause error
) || echo "Subshell exited"
# Safe way to use undefined variables
(
set -u
echo "With default: ${UNDEFINED:-default_value}"
echo "Empty if unset: ${ALSO_UNDEFINED:-}"
)
echo ""
echo "=== set -o pipefail ==="
# Without pipefail
false | true
echo "Without pipefail: $?" # 0 (last command)
# With pipefail
(
set -o pipefail
false | true
echo "Shouldn't reach here"
) || echo "Pipeline failed with pipefail"
echo ""
echo "=== Combined Strict Mode ==="
strict_function() {
set -euo pipefail
echo "Running in strict mode"
# These would all cause immediate exit:
# - false
# - undefined_var
# - false | true
echo "Strict mode passed"
}
strict_function
echo ""
echo "=== PIPESTATUS Array ==="
set -o pipefail
false | true | false
echo "Exit status: $?"
# Can't use PIPESTATUS after set -o pipefail exits
# Save PIPESTATUS immediately
cat /nonexistent 2>/dev/null | grep "x" | wc -l
statuses=("${PIPESTATUS[@]}")
echo "Statuses: ${statuses[*]}"
echo ""
echo "=== Handling Expected Failures ==="
set -e
# Method 1: || true
rm /nonexistent 2>/dev/null || true
echo "Continued after expected failure"
# Method 2: Explicit handling
if ! rm /another_nonexistent 2>/dev/null; then
echo "File didn't exist (expected)"
fi
# Method 3: Temporarily disable
set +e
rm /yet_another 2>/dev/null
exit_code=$?
set -e
echo "Captured exit code: $exit_code"
echo ""
echo "=== Full Script Header ==="
cat << 'EOF'
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
# Script content here...
EOF
echo "(Example script header shown above)"
set -e doesn't trigger in: if conditions, commands before || or &&, commands in pipelines (unless pipefail), or negated commands (! cmd).
Using trap to catch errors and perform cleanup.
#!/bin/bash
# Error traps
echo "=== Basic ERR Trap ==="
err_handler() {
echo "ERROR on line $1: command '$2' failed with exit code $3"
}
trap 'err_handler $LINENO "$BASH_COMMAND" $?' ERR
# This triggers the trap
(
false
) 2>/dev/null
# Reset trap
trap - ERR
echo ""
echo "=== EXIT Trap for Cleanup ==="
cleanup() {
local exit_code=$?
echo "Cleanup: exit code was $exit_code"
rm -f /tmp/test_$$_*
# Return original exit code
exit $exit_code
}
trap cleanup EXIT
# Create temp files
touch /tmp/test_$$_file1
touch /tmp/test_$$_file2
echo "Created temp files"
# Cleanup runs on exit (even from set -e)
echo ""
echo "=== Trap with Line Numbers ==="
set -E # Inherit ERR trap in functions
error_report() {
echo "Error at:"
echo " File: ${BASH_SOURCE[1]:-unknown}"
echo " Line: ${BASH_LINENO[0]:-unknown}"
echo " Func: ${FUNCNAME[1]:-main}"
echo " Command: $BASH_COMMAND"
}
trap error_report ERR
# Test function
risky_function() {
echo "In risky_function"
false # This triggers trap
}
(risky_function) 2>/dev/null || true
trap - ERR
set +E
echo ""
echo "=== Multiple Traps ==="
trap 'echo "Caught INT"' INT
trap 'echo "Caught TERM"' TERM
trap 'echo "Exiting"' EXIT
echo "Traps set"
# Show current traps
echo ""
echo "Current traps:"
trap -p
trap - INT TERM EXIT
echo ""
echo "=== Trap Stack Pattern ==="
# Save and restore traps
push_trap() {
local new_trap=$1
local signal=$2
local old_trap=$(trap -p $signal)
trap "$new_trap; $old_trap" $signal
}
trap 'echo "Original EXIT"' EXIT
push_trap 'echo "Added EXIT"' EXIT
echo "Testing trap stack (triggers on exit)"
echo ""
echo "=== Practical: Transaction Pattern ==="
transaction() {
local rollback_commands=()
rollback() {
echo "Rolling back..."
for ((i=${#rollback_commands[@]}-1; i>=0; i--)); do
eval "${rollback_commands[$i]}"
done
}
trap rollback ERR
# Step 1
mkdir -p /tmp/trans_test
rollback_commands+=("rmdir /tmp/trans_test")
echo "Step 1: created directory"
# Step 2
touch /tmp/trans_test/file
rollback_commands+=("rm /tmp/trans_test/file")
echo "Step 2: created file"
# Simulate success
echo "Transaction complete"
trap - ERR
# Cleanup
rm -rf /tmp/trans_test
}
transaction
Tracing script execution for debugging.
#!/bin/bash
# Debugging with set -x
echo "=== Basic Trace ==="
set -x
name="World"
greeting="Hello, $name"
echo "$greeting"
set +x
echo ""
echo "=== Custom PS4 Prompt ==="
# PS4 controls trace output format
export PS4='+ ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
result=$((5 + 3))
set +x
echo "Result: $result"
# Reset PS4
PS4='+ '
echo ""
echo "=== Trace Specific Section ==="
process_data() {
local input=$1
# Only trace this section
set -x
local processed=$(echo "$input" | tr 'a-z' 'A-Z')
set +x
echo "$processed"
}
output=$(process_data "hello world")
echo "Output: $output"
echo ""
echo "=== Debug to File ==="
# Redirect trace to file
exec 2>/tmp/debug.log
set -x
for i in 1 2 3; do
echo "Item $i"
done > /dev/null
set +x
exec 2>&1
echo "Debug output saved to /tmp/debug.log"
head -5 /tmp/debug.log
rm /tmp/debug.log
echo ""
echo "=== Conditional Debug ==="
DEBUG=${DEBUG:-false}
debug() {
if $DEBUG; then
echo "DEBUG: $*" >&2
fi
}
debug "This is hidden by default"
DEBUG=true
debug "This would show"
DEBUG=false
echo ""
echo "=== BASH_XTRACEFD ==="
# Redirect trace to specific fd
exec 3>/tmp/trace.log
BASH_XTRACEFD=3
set -x
echo "Traced to fd 3" > /dev/null
set +x
exec 3>&-
unset BASH_XTRACEFD
echo "Trace in /tmp/trace.log:"
cat /tmp/trace.log
rm /tmp/trace.log
echo ""
echo "=== Debug Function Calls ==="
func_a() {
echo "In func_a"
func_b
}
func_b() {
echo "In func_b"
echo "Call stack: ${FUNCNAME[*]}"
echo "Line numbers: ${BASH_LINENO[*]}"
}
func_a
echo ""
echo "=== set -v vs set -x ==="
echo "set -v shows lines as read:"
set -v
name="test"
set +v
echo ""
echo "set -x shows expanded commands:"
set -x
name="test"
set +x
Validating arguments and user input.
#!/bin/bash
# Input validation
echo "=== Check Required Arguments ==="
demo_func() {
if [[ $# -lt 2 ]]; then
echo "Usage: demo_func <arg1> <arg2>" >&2
return 1
fi
local arg1=$1
local arg2=$2
echo "Got: $arg1, $arg2"
}
demo_func "one" || true
demo_func "one" "two"
echo ""
echo "=== Validate Argument Types ==="
is_number() {
[[ "$1" =~ ^-?[0-9]+$ ]]
}
is_positive() {
[[ "$1" =~ ^[0-9]+$ ]] && [[ "$1" -gt 0 ]]
}
is_ip_address() {
local ip=$1
local IFS='.'
local -a octets=($ip)
[[ ${#octets[@]} -eq 4 ]] || return 1
for octet in "${octets[@]}"; do
[[ "$octet" =~ ^[0-9]+$ ]] || return 1
((octet >= 0 && octet <= 255)) || return 1
done
}
# Tests
for val in "42" "-5" "abc" "3.14"; do
is_number "$val" && echo "$val: number" || echo "$val: not number"
done
echo ""
for ip in "192.168.1.1" "256.1.1.1" "not.an.ip"; do
is_ip_address "$ip" && echo "$ip: valid" || echo "$ip: invalid"
done
echo ""
echo "=== Validate File Arguments ==="
check_input_file() {
local file=$1
if [[ -z "$file" ]]; then
echo "Error: no file specified" >&2
return 1
fi
if [[ ! -e "$file" ]]; then
echo "Error: file not found: $file" >&2
return 1
fi
if [[ ! -f "$file" ]]; then
echo "Error: not a regular file: $file" >&2
return 1
fi
if [[ ! -r "$file" ]]; then
echo "Error: file not readable: $file" >&2
return 1
fi
return 0
}
check_input_file "/etc/passwd" && echo "/etc/passwd: OK"
check_input_file "/nonexistent" || true
check_input_file "/tmp" || true
echo ""
echo "=== getopts for Option Parsing ==="
parse_options() {
local verbose=false
local output=""
local count=1
OPTIND=1
while getopts "vo:n:" opt; do
case $opt in
v) verbose=true ;;
o) output="$OPTARG" ;;
n)
if ! is_positive "$OPTARG"; then
echo "Error: -n requires positive number" >&2
return 1
fi
count="$OPTARG"
;;
?) return 1 ;;
esac
done
shift $((OPTIND - 1))
echo "verbose=$verbose output='$output' count=$count"
echo "Remaining args: $*"
}
parse_options -v -o out.txt -n 5 file1 file2
echo ""
echo "=== Sanitize Input ==="
sanitize_filename() {
local name=$1
# Remove dangerous characters
name="${name//[^a-zA-Z0-9._-]/}"
# Prevent directory traversal
name="${name#/}"
name="${name#../}"
echo "$name"
}
for input in "normal.txt" "../etc/passwd" "file; rm -rf /" "good-file_1.txt"; do
echo "$input -> $(sanitize_filename "$input")"
done
Structured logging and informative error messages.
#!/bin/bash
# Logging and error messages
echo "=== Basic Logging Functions ==="
# Log levels
LOG_ERROR=0
LOG_WARN=1
LOG_INFO=2
LOG_DEBUG=3
LOG_LEVEL=${LOG_LEVEL:-$LOG_INFO}
log() {
local level=$1
local msg=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
if [[ $level -le $LOG_LEVEL ]]; then
echo "[$timestamp] $msg" >&2
fi
}
log_error() { log $LOG_ERROR "[ERROR] $1"; }
log_warn() { log $LOG_WARN "[WARN] $1"; }
log_info() { log $LOG_INFO "[INFO] $1"; }
log_debug() { log $LOG_DEBUG "[DEBUG] $1"; }
log_info "Application started"
log_debug "Debug info (hidden at INFO level)"
log_warn "Something to watch"
log_error "Something went wrong"
echo ""
echo "=== Colored Output ==="
# Colors
RED='\033[0;31m'
YELLOW='\033[0;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_error() { echo -e "${RED}ERROR: $1${NC}" >&2; }
print_warn() { echo -e "${YELLOW}WARNING: $1${NC}" >&2; }
print_info() { echo -e "${GREEN}INFO: $1${NC}"; }
print_debug() { echo -e "${BLUE}DEBUG: $1${NC}"; }
print_info "Operation successful"
print_warn "Deprecated feature"
print_error "Connection failed"
echo ""
echo "=== Context in Error Messages ==="
error_with_context() {
local msg=$1
local file=${BASH_SOURCE[1]:-unknown}
local line=${BASH_LINENO[0]:-?}
local func=${FUNCNAME[1]:-main}
echo "ERROR in $func() at $file:$line: $msg" >&2
}
failing_function() {
error_with_context "Something went wrong"
}
failing_function
echo ""
echo "=== Log to File ==="
LOGFILE="/tmp/script.log"
init_logging() {
exec 3>> "$LOGFILE"
log_info "Logging initialized to $LOGFILE"
}
close_logging() {
exec 3>&-
}
file_log() {
echo "[$(date '+%H:%M:%S')] $1" >&3
}
init_logging
file_log "This goes to log file"
file_log "Another message"
close_logging
echo "Log file contents:"
cat "$LOGFILE"
rm "$LOGFILE"
echo ""
echo "=== Progress Reporting ==="
progress() {
local current=$1
local total=$2
local width=40
local pct=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf "\r["
printf "%${filled}s" | tr ' ' '#'
printf "%${empty}s" | tr ' ' '-'
printf "] %3d%%" $pct
}
echo "Progress bar:"
for i in {1..10}; do
progress $i 10
sleep 0.1
done
echo ""
echo ""
echo "=== Structured Error Info ==="
create_error() {
local code=$1
local msg=$2
local details=$3
cat << EOF
{
"error": {
"code": $code,
"message": "$msg",
"details": "$details",
"timestamp": "$(date -Iseconds)"
}
}
EOF
}
create_error 404 "File not found" "/path/to/missing"
Validating assumptions with assertion functions.
#!/bin/bash
# Assertions and preconditions
echo "=== Basic Assert ==="
assert() {
local condition=$1
local message=${2:-"Assertion failed"}
if ! eval "$condition"; then
echo "ASSERT FAILED: $message" >&2
echo " Condition: $condition" >&2
echo " Location: ${BASH_SOURCE[1]}:${BASH_LINENO[0]}" >&2
return 1
fi
}
# Tests
assert '[[ 1 -eq 1 ]]' "1 should equal 1"
echo "Assertion passed"
assert '[[ -d /tmp ]]' "/tmp should exist"
echo "Directory assertion passed"
# This would fail:
# assert '[[ 1 -eq 2 ]]' "1 should equal 2"
echo ""
echo "=== Typed Assertions ==="
assert_equals() {
local expected=$1
local actual=$2
local msg=${3:-"Values should be equal"}
if [[ "$expected" != "$actual" ]]; then
echo "ASSERT FAILED: $msg" >&2
echo " Expected: $expected" >&2
echo " Actual: $actual" >&2
return 1
fi
}
assert_not_empty() {
local value=$1
local msg=${2:-"Value should not be empty"}
if [[ -z "$value" ]]; then
echo "ASSERT FAILED: $msg" >&2
return 1
fi
}
assert_file_exists() {
local file=$1
if [[ ! -f "$file" ]]; then
echo "ASSERT FAILED: File should exist: $file" >&2
return 1
fi
}
# Tests
assert_equals "hello" "hello" "Strings match"
assert_not_empty "value" "Has content"
assert_file_exists "/etc/passwd"
echo "All typed assertions passed"
echo ""
echo "=== Preconditions ==="
require_root() {
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This script must be run as root" >&2
return 1
fi
}
require_command() {
local cmd=$1
if ! command -v "$cmd" &>/dev/null; then
echo "ERROR: Required command not found: $cmd" >&2
return 1
fi
}
require_var() {
local var_name=$1
local var_value="${!var_name}"
if [[ -z "$var_value" ]]; then
echo "ERROR: Required variable not set: $var_name" >&2
return 1
fi
}
# Tests
require_command "bash" && echo "bash found"
require_command "ls" && echo "ls found"
MY_VAR="set"
require_var "MY_VAR" && echo "MY_VAR is set"
echo ""
echo "=== Contract Functions ==="
# Require preconditions
divide() {
local numerator=$1
local denominator=$2
# Preconditions
assert_not_empty "$numerator" "Numerator required"
assert_not_empty "$denominator" "Denominator required"
assert '[[ "$denominator" != "0" ]]' "Cannot divide by zero"
echo $((numerator / denominator))
}
result=$(divide 10 2)
echo "10 / 2 = $result"
# This would fail:
# divide 10 0
echo ""
echo "=== Test Mode ==="
run_tests() {
local failures=0
# Test 1
if assert_equals "abc" "abc" "Test 1: string compare"; then
echo "✓ Test 1 passed"
else
((failures++))
fi
# Test 2
if assert '[[ 5 -gt 3 ]]' "Test 2: numeric compare"; then
echo "✓ Test 2 passed"
else
((failures++))
fi
# Test 3
if assert_file_exists "/etc/passwd"; then
echo "✓ Test 3 passed"
else
((failures++))
fi
echo ""
if [[ $failures -eq 0 ]]; then
echo "All tests passed!"
else
echo "$failures test(s) failed"
fi
return $failures
}
run_tests
Using shellcheck and other tools for code quality.
#!/bin/bash
# Static analysis and code quality
echo "=== Common Issues shellcheck Catches ==="
cat << 'EOF'
# Issue 1: Unquoted variables
file=$1
cat $file # SC2086: Double quote to prevent globbing
# Issue 2: Useless use of cat
cat file | grep pattern # SC2002: Useless cat
# Issue 3: == in [ ]
if [ "$a" == "$b" ] # SC2039: == not POSIX, use =
# Issue 4: Unused variables
unused="value" # SC2034: Unused variable
# Issue 5: Command in [ ]
if [ grep -q "x" file ] # SC2046: Use $() for command
EOF
echo ""
echo "=== Running shellcheck ==="
# Check if shellcheck is installed
if command -v shellcheck &>/dev/null; then
# Create test script
cat > /tmp/test_script.sh << 'EOF'
#!/bin/bash
file=$1
cat $file
EOF
echo "Checking /tmp/test_script.sh:"
shellcheck /tmp/test_script.sh || true
rm /tmp/test_script.sh
else
echo "shellcheck not installed"
echo "Install with: apt install shellcheck"
fi
echo ""
echo "=== Syntax Check with bash -n ==="
# Create scripts
cat > /tmp/good.sh << 'EOF'
#!/bin/bash
echo "Hello"
if [[ -f /tmp ]]; then
echo "exists"
fi
EOF
cat > /tmp/bad.sh << 'EOF'
#!/bin/bash
echo "Hello"
if [[ -f /tmp ]]; then
echo "missing fi"
EOF
echo "Checking good.sh:"
if bash -n /tmp/good.sh; then
echo " Syntax OK"
fi
echo ""
echo "Checking bad.sh:"
bash -n /tmp/bad.sh 2>&1 || true
rm /tmp/good.sh /tmp/bad.sh
echo ""
echo "=== Best Practices ==="
cat << 'EOF'
# 1. Always quote variables
echo "$variable"
# 2. Use [[ ]] instead of [ ]
if [[ "$a" == "$b" ]]; then
# 3. Use $() instead of backticks
result=$(command)
# 4. Use arrays for lists
files=(*.txt)
for f in "${files[@]}"; do
# 5. Check command existence
if command -v git &>/dev/null; then
# 6. Use local in functions
myfunc() {
local var="value"
}
# 7. Always use set -euo pipefail
set -euo pipefail
EOF
echo ""
echo "=== Defensive Programming ==="
cat << 'EOF'
# Default values
: "${CONFIG:=/etc/default.conf}"
# Readonly for constants
readonly MAX_RETRIES=3
# Validate early
[[ -f "$input" ]] || die "Input not found"
# Use arrays properly
files=("file with spaces.txt")
for f in "${files[@]}"; do # Not $files or ${files[*]}
EOF
echo ""
echo "=== Portable Code Tips ==="
echo "For POSIX compatibility:"
echo " - Use [ ] not [[ ]]"
echo " - Use \$(cmd) not \$((expr))"
echo " - Use = not == for string compare"
echo " - Avoid arrays and associative arrays"
shellcheck is essential for Bash development. Install with apt install shellcheck or brew install shellcheck.
Making scripts resilient to transient failures.
#!/bin/bash
# Retry and resilience patterns
echo "=== Basic Retry ==="
retry() {
local max_attempts=$1
local delay=$2
shift 2
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
echo "Attempt $attempt of $max_attempts..."
if "$@"; then
echo "Success on attempt $attempt"
return 0
fi
if [[ $attempt -lt $max_attempts ]]; then
echo "Failed, waiting ${delay}s..."
sleep "$delay"
fi
((attempt++))
done
echo "All $max_attempts attempts failed"
return 1
}
# Test with command that might fail
flaky_command() {
# Simulate 50% failure rate
[[ $((RANDOM % 2)) -eq 0 ]]
}
retry 3 1 flaky_command || true
echo ""
echo "=== Exponential Backoff ==="
retry_backoff() {
local max_attempts=$1
shift
local attempt=1
local delay=1
while [[ $attempt -le $max_attempts ]]; do
echo "Attempt $attempt (delay: ${delay}s)..."
if "$@"; then
return 0
fi
if [[ $attempt -lt $max_attempts ]]; then
sleep "$delay"
delay=$((delay * 2)) # Exponential
fi
((attempt++))
done
return 1
}
echo "Exponential backoff pattern defined"
echo ""
echo "=== Circuit Breaker Pattern ==="
declare -A CIRCUIT_STATE
declare -A CIRCUIT_FAILURES
circuit_call() {
local name=$1
shift
local state=${CIRCUIT_STATE[$name]:-closed}
local failures=${CIRCUIT_FAILURES[$name]:-0}
local threshold=3
if [[ "$state" == "open" ]]; then
echo "Circuit $name is OPEN, failing fast"
return 1
fi
if "$@"; then
CIRCUIT_FAILURES[$name]=0
return 0
else
((CIRCUIT_FAILURES[$name]++))
if [[ ${CIRCUIT_FAILURES[$name]} -ge $threshold ]]; then
CIRCUIT_STATE[$name]="open"
echo "Circuit $name OPENED after $threshold failures"
fi
return 1
fi
}
# Test circuit breaker
for i in {1..5}; do
circuit_call "api" false || true
done
echo ""
echo "=== Timeout Wrapper ==="
with_timeout() {
local timeout=$1
shift
timeout "$timeout" "$@"
local status=$?
if [[ $status -eq 124 ]]; then
echo "Command timed out after ${timeout}s" >&2
fi
return $status
}
with_timeout 2 sleep 1 && echo "Completed in time"
with_timeout 1 sleep 5 || echo "Timed out"
echo ""
echo "=== Fallback Pattern ==="
with_fallback() {
local primary=$1
local fallback=$2
if eval "$primary"; then
return 0
fi
echo "Primary failed, trying fallback..."
eval "$fallback"
}
# Test fallback
with_fallback 'ls /nonexistent 2>/dev/null' 'echo "Using default"'
echo ""
echo "=== Health Check ==="
health_check() {
local name=$1
local check=$2
if eval "$check"; then
echo "✓ $name: healthy"
return 0
else
echo "✗ $name: unhealthy"
return 1
fi
}
# Run health checks
errors=0
health_check "Disk space" '[[ $(df -P / | awk "NR==2 {print \$5}" | tr -d %) -lt 90 ]]' || ((errors++))
health_check "Memory" '[[ $(free | awk "/Mem:/ {print int(\$3/\$2*100)}") -lt 90 ]]' || ((errors++))
health_check "/tmp writable" '[[ -w /tmp ]]' || ((errors++))
echo ""
echo "Health checks: $errors failures"
A reusable library for error handling and debugging.
#!/bin/bash
# Complete error handling library
#######################################
# Configuration
#######################################
set -euo pipefail
readonly SCRIPT_NAME="${0##*/}"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
#######################################
# Logging
#######################################
declare -r LOG_ERROR=0 LOG_WARN=1 LOG_INFO=2 LOG_DEBUG=3
LOG_LEVEL=${LOG_LEVEL:-$LOG_INFO}
_log() {
local level=$1 msg=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
[[ $level -le $LOG_LEVEL ]] && echo "[$timestamp] $msg" >&2
}
log_error() { _log $LOG_ERROR "[ERROR] $1"; }
log_warn() { _log $LOG_WARN "[WARN] $1"; }
log_info() { _log $LOG_INFO "[INFO] $1"; }
log_debug() { _log $LOG_DEBUG "[DEBUG] $1"; }
#######################################
# Error Handling
#######################################
die() {
log_error "$1"
exit "${2:-1}"
}
err_trap() {
local exit_code=$?
local line=$1
local command=$2
log_error "Command '$command' failed at line $line (exit $exit_code)"
}
trap 'err_trap $LINENO "$BASH_COMMAND"' ERR
#######################################
# Assertions
#######################################
assert() {
local cond=$1 msg=${2:-"Assertion failed"}
eval "$cond" || die "$msg"
}
require_command() {
command -v "$1" &>/dev/null || die "Required: $1"
}
require_var() {
[[ -n "${!1:-}" ]] || die "Required variable: $1"
}
#######################################
# Resilience
#######################################
retry() {
local attempts=$1 delay=$2
shift 2
for ((i=1; i<=attempts; i++)); do
"$@" && return 0
[[ $i -lt $attempts ]] && sleep "$delay"
done
return 1
}
with_timeout() {
local secs=$1
shift
timeout "$secs" "$@"
}
#######################################
# Cleanup
#######################################
declare -a CLEANUP_COMMANDS=()
cleanup_add() {
CLEANUP_COMMANDS+=("$1")
}
cleanup_run() {
local exit_code=$?
for ((i=${#CLEANUP_COMMANDS[@]}-1; i>=0; i--)); do
eval "${CLEANUP_COMMANDS[$i]}" || true
done
exit $exit_code
}
trap cleanup_run EXIT
#######################################
# Demo
#######################################
main() {
log_info "Starting $SCRIPT_NAME"
# Setup
local tmpdir=$(mktemp -d)
cleanup_add "rm -rf '$tmpdir'"
log_debug "Created temp dir: $tmpdir"
# Preconditions
require_command bash
require_command grep
# Work
log_info "Performing operations..."
echo "test" > "$tmpdir/file.txt"
assert '[[ -f "$tmpdir/file.txt" ]]' "File should exist"
# Simulate retry
local count=0
retry 3 1 bash -c '((++count > 1))' && log_info "Retry succeeded"
log_info "Operations complete"
# Cleanup runs automatically on exit
}
echo "========================================"
echo " Error Handling Library Demo"
echo "========================================"
echo ""
main
echo ""
echo "========================================"
# Strict mode header
set -euo pipefail
# Error trap
trap 'echo "Error at $LINENO"' ERR
# Cleanup trap
trap cleanup EXIT
# Die function
die() { echo "ERROR: $1" >&2; exit 1; }
# Check command exists
command -v git &>/dev/null || die "git required"
# Check variable set
[[ -n "${VAR:-}" ]] || die "VAR required"
| Task | Code |
|---|---|
| Exit on error | set -e |
| Undefined var error | set -u |
| Pipeline fail | set -o pipefail |
| Debug trace | set -x |
| Syntax check | bash -n script.sh |
| Static analysis | shellcheck script.sh |
| Default value | ${VAR:-default} |
| Required value | ${VAR:?error msg} |
set -euo pipefail