Smithery Logo
MCPsSkillsDocsPricing
Login
NewFlame, an assistant that learns and improves. Available onTelegramSlack
    timbuchinger

    bash-defensive-patterns

    timbuchinger/bash-defensive-patterns
    Coding

    About

    SKILL.md

    Install

    • Telegram
      Telegram
    • Slack
      Slack
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    • Download skill
    ├─
    ├─
    └─
    Smithery Logo

    Give agents more agency

    Resources

    DocumentationPrivacy PolicySystem Status

    Company

    PricingAboutBlog

    Connect

    © 2026 Smithery. All rights reserved.

    About

    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...

    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

    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

    Resources

    • Bash Strict Mode
    • Google Shell Style Guide
    • Defensive BASH Programming
    Recommended Servers
    URL Safety Validator MCP
    URL Safety Validator MCP
    OpenZeppelin
    OpenZeppelin
    vastlint - IAB XML VAST validator and linter
    vastlint - IAB XML VAST validator and linter
    Repository
    timbuchinger/loadout
    Files