Bash Defensive Patterns
Comprehensive guidance for writing production-ready Bash scripts using defensive programming techniques, error handling, and safety best practices to prevent common pitfalls and ensure reliability.
When to Use This Skill
-
Writing production automation scripts
-
Building CI/CD pipeline scripts
-
Creating system administration utilities
-
Developing error-resilient deployment automation
-
Writing scripts that must handle edge cases safely
-
Building maintainable shell script libraries
-
Implementing comprehensive logging and monitoring
-
Creating scripts that must work across different platforms
Core Defensive Principles
- Strict Mode
Enable bash strict mode at the start of every script to catch errors early.
#!/bin/bash set -Eeuo pipefail # Exit on error, unset variables, pipe failures
Key flags:
-
set -E : Inherit ERR trap in functions
-
set -e : Exit on any error (command returns non-zero)
-
set -u : Exit on undefined variable reference
-
set -o pipefail : Pipe fails if any command fails (not just last)
- Error Trapping and Cleanup
Implement proper cleanup on script exit or error.
#!/bin/bash set -Eeuo pipefail
trap 'echo "Error on line $LINENO"' ERR trap 'echo "Cleaning up..."; rm -rf "$TMPDIR"' EXIT
TMPDIR=$(mktemp -d)
Script code here
- Variable Safety
Always quote variables to prevent word splitting and globbing issues.
Wrong - unsafe
cp $source $dest
Correct - safe
cp "$source" "$dest"
Required variables - fail with message if unset
: "${REQUIRED_VAR:?REQUIRED_VAR is not set}"
- Array Handling
Use arrays safely for complex data handling.
Safe array iteration
declare -a items=("item 1" "item 2" "item 3")
for item in "${items[@]}"; do echo "Processing: $item" done
Reading output into array safely
mapfile -t lines < <(some_command) readarray -t numbers < <(seq 1 10)
- Conditional Safety
Use [[ ]] for Bash-specific features, [ ] for POSIX.
Bash - safer
if [[ -f "$file" && -r "$file" ]]; then content=$(<"$file") fi
POSIX - portable
if [ -f "$file" ] && [ -r "$file" ]; then content=$(cat "$file") fi
Test for existence before operations
if [[ -z "${VAR:-}" ]]; then echo "VAR is not set or is empty" fi
Fundamental Patterns
Pattern 1: Safe Script Directory Detection
#!/bin/bash set -Eeuo pipefail
Correctly determine script directory
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"
echo "Script location: $SCRIPT_DIR/$SCRIPT_NAME"
Pattern 2: Comprehensive Function Templat
#!/bin/bash set -Eeuo pipefail
Prefix for functions: handle_, process_, check_, validate_
Include documentation and error handling
validate_file() { local -r file="$1" local -r message="${2:-File not found: $file}"
if [[ ! -f "$file" ]]; then
echo "ERROR: $message" >&2
return 1
fi
return 0
}
process_files() { local -r input_dir="$1" local -r output_dir="$2"
# Validate inputs
[[ -d "$input_dir" ]] || { echo "ERROR: input_dir not a directory" >&2; return 1; }
# Create output directory if needed
mkdir -p "$output_dir" || { echo "ERROR: Cannot create output_dir" >&2; return 1; }
# Process files safely
while IFS= read -r -d '' file; do
echo "Processing: $file"
# Do work
done < <(find "$input_dir" -maxdepth 1 -type f -print0)
return 0
}
Pattern 3: Safe Temporary File Handling
#!/bin/bash set -Eeuo pipefail
trap 'rm -rf -- "$TMPDIR"' EXIT
Create temporary directory
TMPDIR=$(mktemp -d) || { echo "ERROR: Failed to create temp directory" >&2; exit 1; }
Create temporary files in directory
TMPFILE1="$TMPDIR/temp1.txt" TMPFILE2="$TMPDIR/temp2.txt"
Use temporary files
touch "$TMPFILE1" "$TMPFILE2"
echo "Temp files created in: $TMPDIR"
Pattern 4: Robust Argument Parsing
#!/bin/bash set -Eeuo pipefail
Default values
VERBOSE=false DRY_RUN=false OUTPUT_FILE="" THREADS=4
usage() { cat <<EOF Usage: $0 [OPTIONS]
Options: -v, --verbose Enable verbose output -d, --dry-run Run without making changes -o, --output FILE Output file path -j, --jobs NUM Number of parallel jobs -h, --help Show this help message EOF exit "${1:-0}" }
Parse arguments
while [[ $# -gt 0 ]]; do case "$1" in -v|--verbose) VERBOSE=true shift ;; -d|--dry-run) DRY_RUN=true shift ;; -o|--output) OUTPUT_FILE="$2" shift 2 ;; -j|--jobs) THREADS="$2" shift 2 ;; -h|--help) usage 0 ;; --) shift break ;; *) echo "ERROR: Unknown option: $1" >&2 usage 1 ;; esac done
Validate required arguments
[[ -n "$OUTPUT_FILE" ]] || { echo "ERROR: -o/--output is required" >&2; usage 1; }
Pattern 5: Structured Logging
#!/bin/bash set -Eeuo pipefail
Logging functions
log_info() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*" >&2 }
log_warn() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*" >&2 }
log_error() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2 }
log_debug() { if [[ "${DEBUG:-0}" == "1" ]]; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] DEBUG: $*" >&2 fi }
Usage
log_info "Starting script" log_debug "Debug information" log_warn "Warning message" log_error "Error occurred"
Pattern 6: Process Orchestration with Signals
#!/bin/bash set -Eeuo pipefail
Track background processes
PIDS=()
cleanup() { log_info "Shutting down..."
# Terminate all background processes
for pid in "${PIDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null || true
fi
done
# Wait for graceful shutdown
for pid in "${PIDS[@]}"; do
wait "$pid" 2>/dev/null || true
done
}
trap cleanup SIGTERM SIGINT
Start background tasks
background_task & PIDS+=($!)
another_task & PIDS+=($!)
Wait for all background processes
wait
Pattern 7: Safe File Operations
#!/bin/bash set -Eeuo pipefail
Use -i flag to move safely without overwriting
safe_move() { local -r source="$1" local -r dest="$2"
if [[ ! -e "$source" ]]; then
echo "ERROR: Source does not exist: $source" >&2
return 1
fi
if [[ -e "$dest" ]]; then
echo "ERROR: Destination already exists: $dest" >&2
return 1
fi
mv "$source" "$dest"
}
Safe directory cleanup
safe_rmdir() { local -r dir="$1"
if [[ ! -d "$dir" ]]; then
echo "ERROR: Not a directory: $dir" >&2
return 1
fi
# Use -I flag to prompt before rm (BSD/GNU compatible)
rm -rI -- "$dir"
}
Atomic file writes
atomic_write() { local -r target="$1" local -r tmpfile tmpfile=$(mktemp) || return 1
# Write to temp file first
cat > "$tmpfile"
# Atomic rename
mv "$tmpfile" "$target"
}
Pattern 8: Idempotent Script Design
#!/bin/bash set -Eeuo pipefail
Check if resource already exists
ensure_directory() { local -r dir="$1"
if [[ -d "$dir" ]]; then
log_info "Directory already exists: $dir"
return 0
fi
mkdir -p "$dir" || {
log_error "Failed to create directory: $dir"
return 1
}
log_info "Created directory: $dir"
}
Ensure configuration state
ensure_config() { local -r config_file="$1" local -r default_value="$2"
if [[ ! -f "$config_file" ]]; then
echo "$default_value" > "$config_file"
log_info "Created config: $config_file"
fi
}
Rerunning script multiple times should be safe
ensure_directory "/var/cache/myapp" ensure_config "/etc/myapp/config" "DEBUG=false"
Pattern 9: Safe Command Substitution
#!/bin/bash set -Eeuo pipefail
Use $() instead of backticks
name=$(<"$file") # Modern, safe variable assignment from file output=$(command -v python3) # Get command location safely
Handle command substitution with error checking
result=$(command -v node) || { log_error "node command not found" return 1 }
For multiple lines
mapfile -t lines < <(grep "pattern" "$file")
NUL-safe iteration
while IFS= read -r -d '' file; do echo "Processing: $file" done < <(find /path -type f -print0)
Pattern 10: Dry-Run Support
#!/bin/bash set -Eeuo pipefail
DRY_RUN="${DRY_RUN:-false}"
run_cmd() { if [[ "$DRY_RUN" == "true" ]]; then echo "[DRY RUN] Would execute: $*" return 0 fi
"$@"
}
Usage
run_cmd cp "$source" "$dest" run_cmd rm "$file" run_cmd chown "$owner" "$target"
Advanced Defensive Techniques
Named Parameters Pattern
#!/bin/bash set -Eeuo pipefail
process_data() { local input_file="" local output_dir="" local format="json"
# Parse named parameters
while [[ $# -gt 0 ]]; do
case "$1" in
--input=*)
input_file="${1#*=}"
;;
--output=*)
output_dir="${1#*=}"
;;
--format=*)
format="${1#*=}"
;;
*)
echo "ERROR: Unknown parameter: $1" >&2
return 1
;;
esac
shift
done
# Validate required parameters
[[ -n "$input_file" ]] || { echo "ERROR: --input is required" >&2; return 1; }
[[ -n "$output_dir" ]] || { echo "ERROR: --output is required" >&2; return 1; }
}
Dependency Checking
#!/bin/bash set -Eeuo pipefail
check_dependencies() { local -a missing_deps=() local -a required=("jq" "curl" "git")
for cmd in "${required[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
missing_deps+=("$cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
echo "ERROR: Missing required commands: ${missing_deps[*]}" >&2
return 1
fi
}
check_dependencies
Best Practices Summary
-
Always use strict mode - set -Eeuo pipefail
-
Quote all variables - "$variable" prevents word splitting
-
Use [[]] conditionals - More robust than [ ]
-
Implement error trapping - Catch and handle errors gracefully
-
Validate all inputs - Check file existence, permissions, formats
-
Use functions for reusability - Prefix with meaningful names
-
Implement structured logging - Include timestamps and levels
-
Support dry-run mode - Allow users to preview changes
-
Handle temporary files safely - Use mktemp, cleanup with trap
-
Design for idempotency - Scripts should be safe to rerun
-
Document requirements - List dependencies and minimum versions
-
Test error paths - Ensure error handling works correctly
-
Use command -v
-
Safer than which for checking executables
-
Prefer printf over echo - More predictable across systems
Resources
-
Bash Strict Mode: http://redsymbol.net/articles/unofficial-bash-strict-mode/
-
Google Shell Style Guide: https://google.github.io/styleguide/shellguide.html
-
Defensive BASH Programming: https://www.lifepipe.net/