bash-defensive-patterns

Production-grade Bash scripting with strict error handling, defensive patterns, and safety best practices. Covers 10 fundamental patterns including strict mode ( set -Eeuo pipefail ), error trapping, variable quoting, array handling, and safe temporary file management Includes advanced techniques for argument parsing, structured logging, process orchestration with signals, and idempotent script design Provides templates for function definitions, file operations, command substitution, and dry-run support to prevent common pitfalls Emphasizes input validation, dependency checking, and cleanup mechanisms to ensure reliability across different platforms and edge cases

INSTALLATION
npx skills add https://github.com/wshobson/agents --skill bash-defensive-patterns
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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.

#!/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.

#!/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.

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

# 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 - safer

if [[ -f "$file" &#x26;&#x26; -r "$file" ]]; then

    content=$(<"$file")

fi

# POSIX - portable

if [ -f "$file" ] &#x26;&#x26; [ -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]}")" &#x26;&#x26; 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" >&#x26;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" >&#x26;2; return 1; }

    # Create output directory if needed

    mkdir -p "$output_dir" || { echo "ERROR: Cannot create output_dir" >&#x26;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" >&#x26;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" >&#x26;2

            usage 1

            ;;

    esac

done

# Validate required arguments

[[ -n "$OUTPUT_FILE" ]] || { echo "ERROR: -o/--output is required" >&#x26;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: $*" >&#x26;2

}

log_warn() {

    echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*" >&#x26;2

}

log_error() {

    echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&#x26;2

}

log_debug() {

    if [[ "${DEBUG:-0}" == "1" ]]; then

        echo "[$(date +'%Y-%m-%d %H:%M:%S')] DEBUG: $*" >&#x26;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 &#x26;

PIDS+=($!)

another_task &#x26;

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" >&#x26;2

        return 1

    fi

    if [[ -e "$dest" ]]; then

        echo "ERROR: Destination already exists: $dest" >&#x26;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" >&#x26;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" >&#x26;2

                return 1

                ;;

        esac

        shift

    done

    # Validate required parameters

    [[ -n "$input_file" ]] || { echo "ERROR: --input is required" >&#x26;2; return 1; }

    [[ -n "$output_dir" ]] || { echo "ERROR: --output is required" >&#x26;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" &#x26;>/dev/null; then

            missing_deps+=("$cmd")

        fi

    done

    if [[ ${#missing_deps[@]} -gt 0 ]]; then

        echo "ERROR: Missing required commands: ${missing_deps[*]}" >&#x26;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
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card