Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/microck-bash-defensive-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 146% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 77% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 165% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 108% | 0% |
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.
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 functionsset -e: Exit on any error (command returns non-zero)set -u: Exit on undefined variable referenceset -o pipefail: Pipe fails if any command fails (not just last)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
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}"
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)
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
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"
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 }
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"
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; }
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"
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
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" }
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"
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)
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"
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; } }
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 } check_dependencies
set -Eeuo pipefail"$variable" prevents word splittingcommand -v - Safer than which for checking executables| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 13,383 | 14,285 | +7% | 1 | 1 | 0% | 2,547 | 6,265 | +146% | 0 | 0 | — |
case-02 | fail→pass | 19,662 | 17,798 | -9% | 1 | 1 | 0% | 3,692 | 7,133 | +93% | 0 | 0 | — |
case-03 | fail→pass | 19,653 | 18,289 | -7% | 1 | 1 | 0% | 3,982 | 7,047 | +77% | 0 | 0 | — |
case-04 | pass→pass | 14,405 | 16,393 | +14% | 1 | 1 | 0% | 2,514 | 6,254 | +149% | 0 | 0 | — |
case-05 | pass→fail | 13,445 | 24,570 | +83% | 1 | 1 | 0% | 2,509 | 8,148 | +225% | 0 | 0 | — |
case-06 | pass→pass | 15,228 | 15,025 | -1% | 1 | 1 | 0% | 2,615 | 6,098 | +133% | 0 | 0 | — |
case-07 | pass→pass | 11,115 | 6,955 | -37% | 1 | 1 | 0% | 1,923 | 4,760 | +148% | 0 | 0 | — |
case-08 | pass→pass | 8,365 | 6,243 | -25% | 1 | 1 | 0% | 1,446 | 4,505 | +212% | 0 | 0 | — |
case-09 | pass→pass | 6,070 | 4,946 | -19% | 1 | 1 | 0% | 983 | 4,203 | +328% | 0 | 0 | — |
case-10 | pass→pass | 14,197 | 14,574 | +3% | 1 | 1 | 0% | 2,429 | 5,920 | +144% | 0 | 0 | — |
case-11 | pass→pass | 9,606 | 7,627 | -21% | 1 | 1 | 0% | 1,944 | 4,831 | +149% | 0 | 0 | — |
case-12 | pass→pass | 13,799 | 14,093 | +2% | 1 | 1 | 0% | 2,461 | 5,894 | +139% | 0 | 0 | — |
case-13 | fail→pass | 12,355 | 10,502 | -15% | 1 | 1 | 0% | 2,036 | 5,401 | +165% | 0 | 0 | — |
case-14 | pass→pass | 8,164 | 5,780 | -29% | 1 | 1 | 0% | 1,465 | 4,352 | +197% | 0 | 0 | — |
case-15 | pass→pass | 11,721 | 9,580 | -18% | 1 | 1 | 0% | 1,971 | 4,988 | +153% | 0 | 0 | — |
case-16 | pass→pass | 8,525 | 11,334 | +33% | 1 | 1 | 0% | 1,532 | 5,633 | +268% | 0 | 0 | — |
case-17 | fail→pass | 16,323 | 14,622 | -10% | 1 | 1 | 0% | 2,973 | 6,178 | +108% | 0 | 0 | — |
case-18 | pass→pass | 9,511 | 6,940 | -27% | 1 | 1 | 0% | 1,734 | 4,687 | +170% | 0 | 0 | — |
case-19 | pass→pass | 5,536 | 5,672 | +2% | 1 | 1 | 0% | 1,004 | 4,446 | +343% | 0 | 0 | — |
case-20 | pass→pass | 10,411 | 7,055 | -32% | 1 | 1 | 0% | 1,818 | 4,698 | +158% | 0 | 0 | — |
case-21 | pass→pass | 12,092 | 9,170 | -24% | 1 | 1 | 0% | 2,232 | 5,179 | +132% | 0 | 0 | — |
case-22 | pass→pass | 6,886 | 7,178 | +4% | 1 | 1 | 0% | 1,298 | 4,807 | +270% | 0 | 0 | — |
case-23 | pass→pass | 7,302 | 4,021 | -45% | 1 | 1 | 0% | 1,248 | 4,120 | +230% | 0 | 0 | — |
case-24 | pass→pass | 4,573 | 5,392 | +18% | 1 | 1 | 0% | 838 | 4,335 | +417% | 0 | 0 | — |
case-25 | pass→pass | 9,747 | 6,794 | -30% | 1 | 1 | 0% | 1,731 | 4,561 | +163% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +16 percentage points is the difference between those two pass rates over the 25 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.