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...
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.
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 failsCatch 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
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}"
}
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)
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
#!/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"
#!/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
}
#!/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"
#!/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; }
#!/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"
#!/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
#!/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"
}
#!/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"
#!/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)
#!/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"
#!/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; }
}
#!/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
#!/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"
#!/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"
#!/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"
set -Eeuo pipefail"$variable" prevents word splitting[[ ]] conditionals - More robust than [ ]command -v - Safer than which for checking executables