bash-defensive-patterns ยท diff
git:20260307.47a5dbc to git:20260522.be57c0b
2 added, 495 removed. Audit A to A.
---
name: bash-defensive-patterns
description: Master defensive Bash programming techniques for production-grade scripts. Use when writing robust shell scripts, CI/CD pipelines, or system utilities requiring fault tolerance and safety.
---
# 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
-
- ### 1. Strict Mode
-
- Enable bash strict mode at the start of every script to catch errors early.
-
- ```bash
- #!/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)
-
- ### 2. Error Trapping and Cleanup
-
- Implement proper cleanup on script exit or error.
-
- ```bash
- #!/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
- ```
-
- ### 3. Variable Safety
-
- Always quote variables to prevent word splitting and globbing issues.
-
- ```bash
- # Wrong - unsafe
- cp $source $dest
-
- # Correct - safe
- cp "$source" "$dest"
-
- # Required variables - fail with message if unset
- : "${REQUIRED_VAR:?REQUIRED_VAR is not set}"
- ```
-
- ### 4. Array Handling
-
- Use arrays safely for complex data handling.
-
- ```bash
- # 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)
- ```
-
- ### 5. Conditional Safety
-
- Use `[[ ]]` for Bash-specific features, `[ ]` for POSIX.
-
- ```bash
- # 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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
-
- ```bash
- #!/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
- }
+ ## Detailed patterns and worked examples
- check_dependencies
- ```
+ Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
## Best Practices Summary
1. **Always use strict mode** - `set -Eeuo pipefail`
2. **Quote all variables** - `"$variable"` prevents word splitting
3. **Use [[]] conditionals** - More robust than [ ]
4. **Implement error trapping** - Catch and handle errors gracefully
5. **Validate all inputs** - Check file existence, permissions, formats
6. **Use functions for reusability** - Prefix with meaningful names
7. **Implement structured logging** - Include timestamps and levels
8. **Support dry-run mode** - Allow users to preview changes
9. **Handle temporary files safely** - Use mktemp, cleanup with trap
10. **Design for idempotency** - Scripts should be safe to rerun
11. **Document requirements** - List dependencies and minimum versions
12. **Test error paths** - Ensure error handling works correctly
13. **Use `command -v`** - Safer than `which` for checking executables
14. **Prefer printf over echo** - More predictable across systems