bash-defensive-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Bash Defensive Patterns

Bash防御式编程模式

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.
本指南全面介绍如何使用防御式编程技术、错误处理和安全最佳实践编写可用于生产环境的Bash脚本,以避免常见陷阱并确保可靠性。

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
  • 编写生产环境自动化脚本
  • 构建CI/CD流水线脚本
  • 创建系统管理工具
  • 开发具备容错能力的部署自动化脚本
  • 编写需要安全处理边缘情况的脚本
  • 构建可维护的Shell脚本库
  • 实现全面的日志记录与监控
  • 创建可跨不同平台运行的脚本

Core Defensive Principles

核心防御原则

1. Strict Mode

1. 严格模式

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)
在每个脚本开头启用Bash严格模式,以便尽早发现错误。
bash
#!/bin/bash
set -Eeuo pipefail  # 遇到错误、未定义变量、管道失败时退出
关键参数说明:
  • set -E
    : 在函数中继承ERR陷阱
  • set -e
    : 任何命令返回非零值时退出
  • set -u
    : 引用未定义变量时退出
  • set -o pipefail
    : 管道中任意命令失败则整个管道失败(而非仅最后一个命令)

2. Error Trapping and Cleanup

2. 错误捕获与清理

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)
在脚本退出或出错时执行适当的清理操作。
bash
#!/bin/bash
set -Eeuo pipefail

trap 'echo "第$LINENO行发生错误"' ERR
trap 'echo "正在清理..."; rm -rf "$TMPDIR"' EXIT

TMPDIR=$(mktemp -d)

Script code here

脚本代码写在这里

undefined
undefined

3. Variable Safety

3. 变量安全

Always quote variables to prevent word splitting and globbing issues.
bash
undefined
始终给变量加引号,防止单词分割和通配符展开问题。
bash
undefined

Wrong - unsafe

错误示例 - 不安全

cp $source $dest
cp $source $dest

Correct - safe

正确示例 - 安全

cp "$source" "$dest"
cp "$source" "$dest"

Required variables - fail with message if unset

必填变量 - 未设置时提示错误并退出

: "${REQUIRED_VAR:?REQUIRED_VAR is not set}"
undefined
: "${REQUIRED_VAR:?REQUIRED_VAR 未设置}"
undefined

4. Array Handling

4. 数组处理

Use arrays safely for complex data handling.
bash
undefined
使用数组安全处理复杂数据。
bash
undefined

Safe array iteration

安全的数组迭代

declare -a items=("item 1" "item 2" "item 3")
for item in "${items[@]}"; do echo "Processing: $item" done
declare -a items=("item 1" "item 2" "item 3")
for item in "${items[@]}"; do echo "正在处理: $item" done

Reading output into array safely

安全地将命令输出读取到数组中

mapfile -t lines < <(some_command) readarray -t numbers < <(seq 1 10)
undefined
mapfile -t lines < <(some_command) readarray -t numbers < <(seq 1 10)
undefined

5. Conditional Safety

5. 条件判断安全

Use
[[ ]]
for Bash-specific features,
[ ]
for POSIX.
bash
undefined
使用
[[ ]]
实现Bash特定功能,使用
[ ]
保证POSIX兼容性。
bash
undefined

Bash - safer

Bash环境 - 更安全

if [[ -f "$file" && -r "$file" ]]; then content=$(<"$file") fi
if [[ -f "$file" && -r "$file" ]]; then content=$(<"$file") fi

POSIX - portable

POSIX兼容 - 可移植

if [ -f "$file" ] && [ -r "$file" ]; then content=$(cat "$file") fi
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
undefined
if [[ -z "${VAR:-}" ]]; then echo "VAR未设置或为空" fi
undefined

Fundamental Patterns

基础模式

Pattern 1: Safe Script Directory Detection

模式1:安全检测脚本目录

bash
#!/bin/bash
set -Eeuo pipefail
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"
undefined
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"
echo "脚本位置: $SCRIPT_DIR/$SCRIPT_NAME"
undefined

Pattern 2: Comprehensive Function Templat

模式2:通用函数模板

bash
#!/bin/bash
set -Eeuo pipefail
bash
#!/bin/bash
set -Eeuo pipefail

Prefix for functions: handle_, process_, check_, validate_

函数命名前缀: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
}
undefined
validate_file() { local -r file="$1" local -r message="${2:-未找到文件: $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"
# 验证输入参数
[[ -d "$input_dir" ]] || { echo "ERROR: 输入目录不存在" >&2; return 1; }

# 按需创建输出目录
mkdir -p "$output_dir" || { echo "ERROR: 无法创建输出目录" >&2; return 1; }

# 安全处理文件
while IFS= read -r -d '' file; do
    echo "正在处理: $file"
    # 执行处理逻辑
done < <(find "$input_dir" -maxdepth 1 -type f -print0)

return 0
}
undefined

Pattern 3: Safe Temporary File Handling

模式3:安全临时文件处理

bash
#!/bin/bash
set -Eeuo pipefail

trap 'rm -rf -- "$TMPDIR"' EXIT
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; }
TMPDIR=$(mktemp -d) || { echo "ERROR: 无法创建临时目录" >&2; exit 1; }

Create temporary files in directory

在临时目录中创建临时文件

TMPFILE1="$TMPDIR/temp1.txt" TMPFILE2="$TMPDIR/temp2.txt"
TMPFILE1="$TMPDIR/temp1.txt" TMPFILE2="$TMPDIR/temp2.txt"

Use temporary files

使用临时文件

touch "$TMPFILE1" "$TMPFILE2"
echo "Temp files created in: $TMPDIR"
undefined
touch "$TMPFILE1" "$TMPFILE2"
echo "临时文件已创建在: $TMPDIR"
undefined

Pattern 4: Robust Argument Parsing

模式4:健壮的参数解析

bash
#!/bin/bash
set -Eeuo pipefail
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}" }
VERBOSE=false DRY_RUN=false OUTPUT_FILE="" THREADS=4
usage() { cat <<EOF 使用方法: $0 [OPTIONS]
选项: -v, --verbose 启用详细输出 -d, --dry-run 试运行模式(不实际修改) -o, --output FILE 输出文件路径 -j, --jobs NUM 并行任务数量 -h, --help 显示帮助信息 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
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: 未知选项: $1" >&2 usage 1 ;; esac done

Validate required arguments

验证必填参数

[[ -n "$OUTPUT_FILE" ]] || { echo "ERROR: -o/--output is required" >&2; usage 1; }
undefined
[[ -n "$OUTPUT_FILE" ]] || { echo "ERROR: -o/--output 为必填选项" >&2; usage 1; }
undefined

Pattern 5: Structured Logging

模式5:结构化日志

bash
#!/bin/bash
set -Eeuo pipefail
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 }
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"
undefined
log_info "脚本启动" log_debug "调试信息" log_warn "警告消息" log_error "发生错误"
undefined

Pattern 6: Process Orchestration with Signals

模式6:信号驱动的进程编排

bash
#!/bin/bash
set -Eeuo pipefail
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
PIDS=()
cleanup() { log_info "正在关闭..."
# 终止所有后台进程
for pid in "${PIDS[@]}"; do
    if kill -0 "$pid" 2>/dev/null; then
        kill -TERM "$pid" 2>/dev/null || true
    fi
done

# 等待优雅关闭
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+=($!)
background_task & PIDS+=($!)
another_task & PIDS+=($!)

Wait for all background processes

等待所有后台进程完成

wait
undefined
wait
undefined

Pattern 7: Safe File Operations

模式7:安全文件操作

bash
#!/bin/bash
set -Eeuo pipefail
bash
#!/bin/bash
set -Eeuo pipefail

Use -i flag to move safely without overwriting

使用-i参数安全移动文件,避免覆盖

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_move() { local -r source="$1" local -r dest="$2"
if [[ ! -e "$source" ]]; then
    echo "ERROR: 源文件不存在: $source" >&2
    return 1
fi

if [[ -e "$dest" ]]; then
    echo "ERROR: 目标文件已存在: $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"
}
safe_rmdir() { local -r dir="$1"
if [[ ! -d "$dir" ]]; then
    echo "ERROR: 不是目录: $dir" >&2
    return 1
fi

# 使用-I参数,删除前确认(兼容BSD/GNU)
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"
}
undefined
atomic_write() { local -r target="$1" local -r tmpfile tmpfile=$(mktemp) || return 1
# 先写入临时文件
cat > "$tmpfile"

# 原子重命名
mv "$tmpfile" "$target"
}
undefined

Pattern 8: Idempotent Script Design

模式8:幂等脚本设计

bash
#!/bin/bash
set -Eeuo pipefail
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_directory() { local -r dir="$1"
if [[ -d "$dir" ]]; then
    log_info "目录已存在: $dir"
    return 0
fi

mkdir -p "$dir" || {
    log_error "无法创建目录: $dir"
    return 1
}

log_info "已创建目录: $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
}
ensure_config() { local -r config_file="$1" local -r default_value="$2"
if [[ ! -f "$config_file" ]]; then
    echo "$default_value" > "$config_file"
    log_info "已创建配置文件: $config_file"
fi
}

Rerunning script multiple times should be safe

多次运行脚本应保持安全

ensure_directory "/var/cache/myapp" ensure_config "/etc/myapp/config" "DEBUG=false"
undefined
ensure_directory "/var/cache/myapp" ensure_config "/etc/myapp/config" "DEBUG=false"
undefined

Pattern 9: Safe Command Substitution

模式9:安全命令替换

bash
#!/bin/bash
set -Eeuo pipefail
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
name=$(<"$file") # 从文件读取内容的现代安全方式 output=$(command -v python3) # 安全获取命令路径

Handle command substitution with error checking

带错误检查的命令替换

result=$(command -v node) || { log_error "node command not found" return 1 }
result=$(command -v node) || { log_error "未找到node命令" return 1 }

For multiple lines

处理多行内容

mapfile -t lines < <(grep "pattern" "$file")
mapfile -t lines < <(grep "pattern" "$file")

NUL-safe iteration

基于NUL的安全迭代

while IFS= read -r -d '' file; do echo "Processing: $file" done < <(find /path -type f -print0)
undefined
while IFS= read -r -d '' file; do echo "正在处理: $file" done < <(find /path -type f -print0)
undefined

Pattern 10: Dry-Run Support

模式10:试运行支持

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

    "$@"
}
bash
#!/bin/bash
set -Eeuo pipefail

DRY_RUN="${DRY_RUN:-false}"

run_cmd() {
    if [[ "$DRY_RUN" == "true" ]]; then
        echo "[试运行] 将要执行: $*"
        return 0
    fi

    "$@"
}

Usage

使用示例

run_cmd cp "$source" "$dest" run_cmd rm "$file" run_cmd chown "$owner" "$target"
undefined
run_cmd cp "$source" "$dest" run_cmd rm "$file" run_cmd chown "$owner" "$target"
undefined

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; }
}
bash
#!/bin/bash
set -Eeuo pipefail

process_data() {
    local input_file=""
    local output_dir=""
    local format="json"

    # 解析命名参数
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --input=*)
                input_file="${1#*=}"
                ;;
            --output=*)
                output_dir="${1#*=}"
                ;;
            --format=*)
                format="${1#*=}"
                ;;
            *)
                echo "ERROR: 未知参数: $1" >&2
                return 1
                ;;
        esac
        shift
    done

    # 验证必填参数
    [[ -n "$input_file" ]] || { echo "ERROR: --input 为必填参数" >&2; return 1; }
    [[ -n "$output_dir" ]] || { echo "ERROR: --output 为必填参数" >&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
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_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
  1. 始终使用严格模式 -
    set -Eeuo pipefail
  2. 所有变量加引号 -
    "$variable"
    避免单词分割
  3. 使用[[ ]]进行条件判断 - 比[ ]更健壮
  4. 实现错误捕获 - 优雅地捕获和处理错误
  5. 验证所有输入 - 检查文件存在性、权限、格式
  6. 使用函数提高复用性 - 使用有意义的命名前缀
  7. 实现结构化日志 - 包含时间戳和日志级别
  8. 支持试运行模式 - 允许用户预览变更
  9. 安全处理临时文件 - 使用mktemp,通过trap清理
  10. 设计幂等脚本 - 脚本可安全重复运行
  11. 记录依赖要求 - 列出依赖项和最低版本
  12. 测试错误路径 - 确保错误处理逻辑正常工作
  13. 使用
    command -v
    - 比
    which
    更安全的命令检查方式
  14. 优先使用printf而非echo - 在不同系统上表现更一致

Resources

参考资源