Bash Defensive Patterns

Write bulletproof Bash scripts with production-grade defensive patterns

✨ The solution you've been looking for

Verified
Tested and verified by our team
25450 Stars

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 shell-scripting defensive-programming error-handling production automation ci-cd devops
Repository

See It In Action

Interactive preview & real-world examples

Live Demo
Skill Demo Animation

AI Conversation Simulator

See how users interact with this skill

User Prompt

Help me write a deployment script that safely copies files, validates checksums, and rolls back on failure. It should handle edge cases and provide detailed logging.

Skill Processing

Analyzing request...

Agent Response

A production-ready script with strict mode, error trapping, cleanup handlers, and structured logging that prevents common pitfalls

Quick Start (3 Steps)

Get up and running in minutes

1

Install

claude-code skill install bash-defensive-patterns

claude-code skill install bash-defensive-patterns
2

Config

3

First Trigger

@bash-defensive-patterns help

Commands

CommandDescriptionRequired Args
@bash-defensive-patterns production-ci/cd-pipeline-scriptCreate a robust deployment script that handles failures gracefully and provides comprehensive loggingNone
@bash-defensive-patterns system-administration-utilityBuild a maintainable script for routine system maintenance that can be safely rerunNone
@bash-defensive-patterns data-processing-automationDevelop a fault-tolerant script for processing large datasets with parallel executionNone

Typical Use Cases

Production CI/CD Pipeline Script

Create a robust deployment script that handles failures gracefully and provides comprehensive logging

System Administration Utility

Build a maintainable script for routine system maintenance that can be safely rerun

Data Processing Automation

Develop a fault-tolerant script for processing large datasets with parallel execution

Overview

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.

1#!/bin/bash
2set -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.

1#!/bin/bash
2set -Eeuo pipefail
3
4trap 'echo "Error on line $LINENO"' ERR
5trap 'echo "Cleaning up..."; rm -rf "$TMPDIR"' EXIT
6
7TMPDIR=$(mktemp -d)
8# Script code here

3. Variable Safety

Always quote variables to prevent word splitting and globbing issues.

1# Wrong - unsafe
2cp $source $dest
3
4# Correct - safe
5cp "$source" "$dest"
6
7# Required variables - fail with message if unset
8: "${REQUIRED_VAR:?REQUIRED_VAR is not set}"

4. Array Handling

Use arrays safely for complex data handling.

 1# Safe array iteration
 2declare -a items=("item 1" "item 2" "item 3")
 3
 4for item in "${items[@]}"; do
 5    echo "Processing: $item"
 6done
 7
 8# Reading output into array safely
 9mapfile -t lines < <(some_command)
10readarray -t numbers < <(seq 1 10)

5. Conditional Safety

Use [[ ]] for Bash-specific features, [ ] for POSIX.

 1# Bash - safer
 2if [[ -f "$file" && -r "$file" ]]; then
 3    content=$(<"$file")
 4fi
 5
 6# POSIX - portable
 7if [ -f "$file" ] && [ -r "$file" ]; then
 8    content=$(cat "$file")
 9fi
10
11# Test for existence before operations
12if [[ -z "${VAR:-}" ]]; then
13    echo "VAR is not set or is empty"
14fi

Fundamental Patterns

Pattern 1: Safe Script Directory Detection

1#!/bin/bash
2set -Eeuo pipefail
3
4# Correctly determine script directory
5SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
6SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"
7
8echo "Script location: $SCRIPT_DIR/$SCRIPT_NAME"

Pattern 2: Comprehensive Function Templat

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4# Prefix for functions: handle_*, process_*, check_*, validate_*
 5# Include documentation and error handling
 6
 7validate_file() {
 8    local -r file="$1"
 9    local -r message="${2:-File not found: $file}"
10
11    if [[ ! -f "$file" ]]; then
12        echo "ERROR: $message" >&2
13        return 1
14    fi
15    return 0
16}
17
18process_files() {
19    local -r input_dir="$1"
20    local -r output_dir="$2"
21
22    # Validate inputs
23    [[ -d "$input_dir" ]] || { echo "ERROR: input_dir not a directory" >&2; return 1; }
24
25    # Create output directory if needed
26    mkdir -p "$output_dir" || { echo "ERROR: Cannot create output_dir" >&2; return 1; }
27
28    # Process files safely
29    while IFS= read -r -d '' file; do
30        echo "Processing: $file"
31        # Do work
32    done < <(find "$input_dir" -maxdepth 1 -type f -print0)
33
34    return 0
35}

Pattern 3: Safe Temporary File Handling

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4trap 'rm -rf -- "$TMPDIR"' EXIT
 5
 6# Create temporary directory
 7TMPDIR=$(mktemp -d) || { echo "ERROR: Failed to create temp directory" >&2; exit 1; }
 8
 9# Create temporary files in directory
10TMPFILE1="$TMPDIR/temp1.txt"
11TMPFILE2="$TMPDIR/temp2.txt"
12
13# Use temporary files
14touch "$TMPFILE1" "$TMPFILE2"
15
16echo "Temp files created in: $TMPDIR"

Pattern 4: Robust Argument Parsing

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4# Default values
 5VERBOSE=false
 6DRY_RUN=false
 7OUTPUT_FILE=""
 8THREADS=4
 9
10usage() {
11    cat <<EOF
12Usage: $0 [OPTIONS]
13
14Options:
15    -v, --verbose       Enable verbose output
16    -d, --dry-run       Run without making changes
17    -o, --output FILE   Output file path
18    -j, --jobs NUM      Number of parallel jobs
19    -h, --help          Show this help message
20EOF
21    exit "${1:-0}"
22}
23
24# Parse arguments
25while [[ $# -gt 0 ]]; do
26    case "$1" in
27        -v|--verbose)
28            VERBOSE=true
29            shift
30            ;;
31        -d|--dry-run)
32            DRY_RUN=true
33            shift
34            ;;
35        -o|--output)
36            OUTPUT_FILE="$2"
37            shift 2
38            ;;
39        -j|--jobs)
40            THREADS="$2"
41            shift 2
42            ;;
43        -h|--help)
44            usage 0
45            ;;
46        --)
47            shift
48            break
49            ;;
50        *)
51            echo "ERROR: Unknown option: $1" >&2
52            usage 1
53            ;;
54    esac
55done
56
57# Validate required arguments
58[[ -n "$OUTPUT_FILE" ]] || { echo "ERROR: -o/--output is required" >&2; usage 1; }

Pattern 5: Structured Logging

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4# Logging functions
 5log_info() {
 6    echo "[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*" >&2
 7}
 8
 9log_warn() {
10    echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*" >&2
11}
12
13log_error() {
14    echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
15}
16
17log_debug() {
18    if [[ "${DEBUG:-0}" == "1" ]]; then
19        echo "[$(date +'%Y-%m-%d %H:%M:%S')] DEBUG: $*" >&2
20    fi
21}
22
23# Usage
24log_info "Starting script"
25log_debug "Debug information"
26log_warn "Warning message"
27log_error "Error occurred"

Pattern 6: Process Orchestration with Signals

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4# Track background processes
 5PIDS=()
 6
 7cleanup() {
 8    log_info "Shutting down..."
 9
10    # Terminate all background processes
11    for pid in "${PIDS[@]}"; do
12        if kill -0 "$pid" 2>/dev/null; then
13            kill -TERM "$pid" 2>/dev/null || true
14        fi
15    done
16
17    # Wait for graceful shutdown
18    for pid in "${PIDS[@]}"; do
19        wait "$pid" 2>/dev/null || true
20    done
21}
22
23trap cleanup SIGTERM SIGINT
24
25# Start background tasks
26background_task &
27PIDS+=($!)
28
29another_task &
30PIDS+=($!)
31
32# Wait for all background processes
33wait

Pattern 7: Safe File Operations

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4# Use -i flag to move safely without overwriting
 5safe_move() {
 6    local -r source="$1"
 7    local -r dest="$2"
 8
 9    if [[ ! -e "$source" ]]; then
10        echo "ERROR: Source does not exist: $source" >&2
11        return 1
12    fi
13
14    if [[ -e "$dest" ]]; then
15        echo "ERROR: Destination already exists: $dest" >&2
16        return 1
17    fi
18
19    mv "$source" "$dest"
20}
21
22# Safe directory cleanup
23safe_rmdir() {
24    local -r dir="$1"
25
26    if [[ ! -d "$dir" ]]; then
27        echo "ERROR: Not a directory: $dir" >&2
28        return 1
29    fi
30
31    # Use -I flag to prompt before rm (BSD/GNU compatible)
32    rm -rI -- "$dir"
33}
34
35# Atomic file writes
36atomic_write() {
37    local -r target="$1"
38    local -r tmpfile
39    tmpfile=$(mktemp) || return 1
40
41    # Write to temp file first
42    cat > "$tmpfile"
43
44    # Atomic rename
45    mv "$tmpfile" "$target"
46}

Pattern 8: Idempotent Script Design

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4# Check if resource already exists
 5ensure_directory() {
 6    local -r dir="$1"
 7
 8    if [[ -d "$dir" ]]; then
 9        log_info "Directory already exists: $dir"
10        return 0
11    fi
12
13    mkdir -p "$dir" || {
14        log_error "Failed to create directory: $dir"
15        return 1
16    }
17
18    log_info "Created directory: $dir"
19}
20
21# Ensure configuration state
22ensure_config() {
23    local -r config_file="$1"
24    local -r default_value="$2"
25
26    if [[ ! -f "$config_file" ]]; then
27        echo "$default_value" > "$config_file"
28        log_info "Created config: $config_file"
29    fi
30}
31
32# Rerunning script multiple times should be safe
33ensure_directory "/var/cache/myapp"
34ensure_config "/etc/myapp/config" "DEBUG=false"

Pattern 9: Safe Command Substitution

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4# Use $() instead of backticks
 5name=$(<"$file")  # Modern, safe variable assignment from file
 6output=$(command -v python3)  # Get command location safely
 7
 8# Handle command substitution with error checking
 9result=$(command -v node) || {
10    log_error "node command not found"
11    return 1
12}
13
14# For multiple lines
15mapfile -t lines < <(grep "pattern" "$file")
16
17# NUL-safe iteration
18while IFS= read -r -d '' file; do
19    echo "Processing: $file"
20done < <(find /path -type f -print0)

Pattern 10: Dry-Run Support

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4DRY_RUN="${DRY_RUN:-false}"
 5
 6run_cmd() {
 7    if [[ "$DRY_RUN" == "true" ]]; then
 8        echo "[DRY RUN] Would execute: $*"
 9        return 0
10    fi
11
12    "$@"
13}
14
15# Usage
16run_cmd cp "$source" "$dest"
17run_cmd rm "$file"
18run_cmd chown "$owner" "$target"

Advanced Defensive Techniques

Named Parameters Pattern

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4process_data() {
 5    local input_file=""
 6    local output_dir=""
 7    local format="json"
 8
 9    # Parse named parameters
10    while [[ $# -gt 0 ]]; do
11        case "$1" in
12            --input=*)
13                input_file="${1#*=}"
14                ;;
15            --output=*)
16                output_dir="${1#*=}"
17                ;;
18            --format=*)
19                format="${1#*=}"
20                ;;
21            *)
22                echo "ERROR: Unknown parameter: $1" >&2
23                return 1
24                ;;
25        esac
26        shift
27    done
28
29    # Validate required parameters
30    [[ -n "$input_file" ]] || { echo "ERROR: --input is required" >&2; return 1; }
31    [[ -n "$output_dir" ]] || { echo "ERROR: --output is required" >&2; return 1; }
32}

Dependency Checking

 1#!/bin/bash
 2set -Eeuo pipefail
 3
 4check_dependencies() {
 5    local -a missing_deps=()
 6    local -a required=("jq" "curl" "git")
 7
 8    for cmd in "${required[@]}"; do
 9        if ! command -v "$cmd" &>/dev/null; then
10            missing_deps+=("$cmd")
11        fi
12    done
13
14    if [[ ${#missing_deps[@]} -gt 0 ]]; then
15        echo "ERROR: Missing required commands: ${missing_deps[*]}" >&2
16        return 1
17    fi
18}
19
20check_dependencies

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

What Users Are Saying

Real feedback from the community

Environment Matrix

Dependencies

Bash 4.0+ (recommended for associative arrays)
Standard UNIX utilities (find, grep, mktemp)

Context Window

Token Usage ~3K-8K tokens depending on script complexity and patterns used

Security & Privacy

Information

Author
wshobson
Updated
2026-01-30
Category
system-admin