Initial commit
This commit is contained in:
533
skills/bash-defensive-patterns/SKILL.md
Normal file
533
skills/bash-defensive-patterns/SKILL.md
Normal file
@@ -0,0 +1,533 @@
|
||||
---
|
||||
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/
|
||||
Reference in New Issue
Block a user