mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 17:47:16 +00:00
Fantastic work, Ryan! You delivered an exceptional shell-scripting plugin with comprehensive coverage of defensive programming, static analysis, and testing patterns. Your responsiveness to feedback and commitment to quality are exactly what we value in this project. Thanks for the great contribution! 🎉
534 lines
12 KiB
Markdown
534 lines
12 KiB
Markdown
---
|
|
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
|
|
}
|
|
|
|
check_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
|
|
|
|
- **Bash Strict Mode**: http://redsymbol.net/articles/unofficial-bash-strict-mode/
|
|
- **Google Shell Style Guide**: https://google.github.io/styleguide/shellguide.html
|
|
- **Defensive BASH Programming**: https://www.lifepipe.net/
|