Agent skill
bash-defensive-patterns
Use when writing or reviewing Bash scripts to apply defensive programming patterns including strict mode, proper error handling, safe variable handling, argument parsing, and idempotent design. Ensures scripts are robust, maintainable, and production-ready.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/bash-defensive-patterns
SKILL.md
Bash Defensive Patterns
Overview
Apply defensive programming patterns to write robust, maintainable Bash scripts that handle errors gracefully and fail safely.
Core principle: Scripts should be safe by default, fail fast, and provide clear error messages.
When to Use
- Writing new Bash scripts
- Reviewing existing shell scripts
- Debugging script failures
- Hardening automation scripts
- Creating production-ready scripts
Defensive Foundations
1. Strict Mode (Always Required)
Use at the beginning of every script:
#!/bin/bash
set -Eeuo pipefail
What each flag does:
-e- Exit immediately if any command fails-E- Inherit ERR trap by functions and subshells-u- Treat unset variables as errors-o pipefail- Exit if any command in a pipeline fails
2. Error Trapping
Catch and handle errors gracefully:
#!/bin/bash
set -Eeuo pipefail
cleanup() {
local exit_code=$?
echo "Cleanup triggered with exit code: $exit_code" >&2
# Cleanup operations here
rm -f "$TMPFILE" 2>/dev/null || true
}
trap cleanup EXIT
trap 'echo "Error on line $LINENO" >&2' ERR
3. Variable Quoting
Quote all variable expansions to prevent word splitting and globbing:
# Good - quoted
file_path="/path/to/my file.txt"
cp "$file_path" "$destination"
# Bad - unquoted (breaks with spaces)
cp $file_path $destination
# Use readonly for constants
readonly CONFIG_FILE="/etc/myapp/config"
readonly MAX_RETRIES=3
# Local variables in functions
function_name() {
local -r required_arg="$1"
local optional_arg="${2:-default}"
}
4. Array Handling
Use arrays properly for lists:
# Define arrays
files=("file1.txt" "file2.txt" "file3.txt")
items=("item 1" "item 2" "item 3")
# Iterate safely
for item in "${items[@]}"; do
echo "Processing: $item"
done
# Read output into array safely
mapfile -t lines < <(some_command)
readarray -t numbers < <(seq 1 10)
5. 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 Template
#!/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
# Define 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 examples
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
# Move files 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
# Prompt before deletion
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 examples
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
Real-World Examples
Example 1: Backup Script with Rotation
#!/bin/bash
set -Eeuo pipefail
# Configuration
readonly BACKUP_SOURCE="/var/www/html"
readonly BACKUP_DEST="/backup/website"
readonly MAX_BACKUPS=7
readonly TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Setup logging
log_info() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*" >&2
}
log_error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
}
# Cleanup temporary files on exit
cleanup() {
if [[ -n "${TEMP_ARCHIVE:-}" && -f "$TEMP_ARCHIVE" ]]; then
rm -f "$TEMP_ARCHIVE"
fi
}
trap cleanup EXIT
# Validate source directory exists
[[ -d "$BACKUP_SOURCE" ]] || {
log_error "Source directory not found: $BACKUP_SOURCE"
exit 1
}
# Create backup destination if needed
mkdir -p "$BACKUP_DEST" || {
log_error "Cannot create backup directory: $BACKUP_DEST"
exit 1
}
# Create backup with atomic write
TEMP_ARCHIVE=$(mktemp) || exit 1
log_info "Creating backup archive..."
tar -czf "$TEMP_ARCHIVE" -C "$(dirname "$BACKUP_SOURCE")" "$(basename "$BACKUP_SOURCE")" || {
log_error "Failed to create backup archive"
exit 1
}
# Move to final location atomically
FINAL_BACKUP="$BACKUP_DEST/backup_$TIMESTAMP.tar.gz"
mv "$TEMP_ARCHIVE" "$FINAL_BACKUP"
log_info "Backup saved to: $FINAL_BACKUP"
# Rotate old backups
log_info "Rotating old backups..."
while IFS= read -r -d '' backup; do
rm -f "$backup"
log_info "Removed old backup: $(basename "$backup")"
done < <(find "$BACKUP_DEST" -name "backup_*.tar.gz" -type f -print0 | sort -z | head -z -n -"$MAX_BACKUPS")
log_info "Backup completed successfully"
Example 2: Deploy Script with Validation
#!/bin/bash
set -Eeuo pipefail
# Script configuration
DEPLOY_ENV="${DEPLOY_ENV:-staging}"
DRY_RUN="${DRY_RUN:-false}"
# Validate environment
case "$DEPLOY_ENV" in
staging|production)
readonly DEPLOY_PATH="/var/www/$DEPLOY_ENV"
readonly SERVICE_NAME="app-$DEPLOY_ENV"
;;
*)
echo "ERROR: Invalid environment: $DEPLOY_ENV (must be staging or production)" >&2
exit 1
;;
esac
# Check required commands
check_dependencies() {
local -a required=("rsync" "systemctl" "git")
local -a missing=()
for cmd in "${required[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "ERROR: Missing required commands: ${missing[*]}" >&2
return 1
fi
}
# Execute command with dry-run support
run_cmd() {
if [[ "$DRY_RUN" == "true" ]]; then
echo "[DRY RUN] Would execute: $*"
return 0
fi
"$@"
}
# Main deployment
check_dependencies
echo "Deploying to: $DEPLOY_ENV"
echo "Deploy path: $DEPLOY_PATH"
# Create backup before deployment
BACKUP_DIR="$DEPLOY_PATH.backup.$(date +%Y%m%d_%H%M%S)"
run_cmd cp -a "$DEPLOY_PATH" "$BACKUP_DIR"
# Deploy new code
run_cmd rsync -av --delete ./build/ "$DEPLOY_PATH/"
# Restart service
run_cmd systemctl restart "$SERVICE_NAME"
echo "Deployment completed successfully"
echo "Backup available at: $BACKUP_DIR"
Example 3: Log Processing Pipeline
#!/bin/bash
set -Eeuo pipefail
# Process and analyze log files from multiple sources
readonly LOG_DIR="/var/log/apps"
readonly OUTPUT_FILE="/tmp/log_analysis_$(date +%Y%m%d).txt"
readonly ERROR_THRESHOLD=100
# Process logs safely
process_logs() {
local -r log_pattern="$1"
local error_count=0
while IFS= read -r -d '' logfile; do
if [[ ! -r "$logfile" ]]; then
echo "WARNING: Cannot read file: $logfile" >&2
continue
fi
# Count errors in this log file
local file_errors
file_errors=$(grep -c "ERROR" "$logfile" 2>/dev/null || echo "0")
error_count=$((error_count + file_errors))
echo "$(basename "$logfile"): $file_errors errors"
done < <(find "$LOG_DIR" -name "$log_pattern" -type f -print0)
echo "---"
echo "Total errors: $error_count"
# Alert if threshold exceeded
if [[ $error_count -gt $ERROR_THRESHOLD ]]; then
echo "WARNING: Error count ($error_count) exceeds threshold ($ERROR_THRESHOLD)" >&2
return 1
fi
return 0
}
# Validate log directory exists
[[ -d "$LOG_DIR" ]] || {
echo "ERROR: Log directory not found: $LOG_DIR" >&2
exit 1
}
# Process logs and save results
{
echo "Log Analysis Report - $(date)"
echo "================================"
echo ""
process_logs "*.log"
} > "$OUTPUT_FILE"
echo "Analysis complete. Results saved to: $OUTPUT_FILE"
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 thanwhichfor checking executables - Prefer printf over echo - More predictable across systems
Resources
Didn't find tool you were looking for?