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. - Writing production automation scripts - Building CI/CD pipeline scripts
#!/bin/bash set -Eeuo pipefail # Exit on error, unset variables, pipe failures
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)#!/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
# Wrong - unsafe cp $source $dest # Correct - safe cp "$source" "$dest" # Required variables - fail with message if unset : "${REQUIRED_VAR:?REQUIRED_VAR is not set}"
# 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)
[[ ]] for Bash-specific features, [ ] for POSIX.# 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
#!/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` #!/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` #!/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` #!/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` #!/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` #!/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` #!/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 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"
#!/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` #!/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