Bash防御性编程模式Skill bash-defensive-patterns

本技能专注于Bash脚本的防御性编程技术,包括严格模式、错误捕获、变量安全、临时文件管理等,用于编写生产级自动化脚本、CI/CD管道和系统工具,提高脚本的可靠性和安全性。关键词:Bash防御性编程,错误处理,脚本安全,自动化,CI/CD,系统管理。

DevOps 0 次安装 0 次浏览 更新于 3/22/2026

name: bash-defensive-patterns description: 掌握Bash防御性编程技术,用于生产级脚本。在编写健壮的shell脚本、CI/CD管道或需要容错和安全性的系统工具时使用。

Bash防御性编程模式

使用防御性编程技术、错误处理和安全最佳实践编写生产就绪Bash脚本的综合指南,以防止常见陷阱并确保可靠性。

何时使用此技能

  • 编写生产自动化脚本
  • 构建CI/CD管道脚本
  • 创建系统管理工具
  • 开发容错部署自动化
  • 编写必须安全处理边缘情况的脚本
  • 构建可维护的shell脚本库
  • 实现全面日志记录和监控
  • 创建必须在不同平台上工作的脚本

核心防御性原则

1. 严格模式

在每个脚本开始时启用Bash严格模式,以尽早捕获错误。

#!/bin/bash
set -Eeuo pipefail  # 出错时退出,未设置变量,管道失败

关键标志:

  • set -E:在函数中继承ERR陷阱
  • set -e:任何错误时退出(命令返回非零)
  • set -u:未定义变量引用时退出
  • set -o pipefail:如果任何命令失败,管道失败(不仅是最后一个)

2. 错误捕获和清理

在脚本退出或错误时实现适当清理。

#!/bin/bash
set -Eeuo pipefail

trap 'echo "Error on line $LINENO"' ERR
trap 'echo "Cleaning up..."; rm -rf "$TMPDIR"' EXIT

TMPDIR=$(mktemp -d)
# 脚本代码在这里

3. 变量安全

始终引用变量以防止单词拆分和通配问题。

# 错误 - 不安全
cp $source $dest

# 正确 - 安全
cp "$source" "$dest"

# 必需变量 - 如果未设置,则失败并显示消息
: "${REQUIRED_VAR:?REQUIRED_VAR is not set}"

4. 数组处理

安全使用数组处理复杂数据。

# 安全数组迭代
declare -a items=("item 1" "item 2" "item 3")

for item in "${items[@]}"; do
    echo "Processing: $item"
done

# 安全读取输出到数组
mapfile -t lines < <(some_command)
readarray -t numbers < <(seq 1 10)

5. 条件安全

使用[[ ]]用于Bash特定功能,[ ]用于POSIX。

# Bash - 更安全
if [[ -f "$file" && -r "$file" ]]; then
    content=$(<"$file")
fi

# POSIX - 可移植
if [ -f "$file" ] && [ -r "$file" ]; then
    content=$(cat "$file")
fi

# 操作前测试存在性
if [[ -z "${VAR:-}" ]]; then
    echo "VAR is not set or is empty"
fi

基本模式

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

#!/bin/bash
set -Eeuo pipefail

# 正确确定脚本目录
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"

echo "Script location: $SCRIPT_DIR/$SCRIPT_NAME"

模式2:综合函数模板

#!/bin/bash
set -Eeuo pipefail

# 函数前缀:handle_*, process_*, check_*, validate_*
# 包括文档和错误处理

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"

    # 验证输入
    [[ -d "$input_dir" ]] || { echo "ERROR: input_dir not a directory" >&2; return 1; }

    # 如果需要,创建输出目录
    mkdir -p "$output_dir" || { echo "ERROR: Cannot create output_dir" >&2; return 1; }

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

    return 0
}

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

#!/bin/bash
set -Eeuo pipefail

trap 'rm -rf -- "$TMPDIR"' EXIT

# 创建临时目录
TMPDIR=$(mktemp -d) || { echo "ERROR: Failed to create temp directory" >&2; exit 1; }

# 在目录中创建临时文件
TMPFILE1="$TMPDIR/temp1.txt"
TMPFILE2="$TMPDIR/temp2.txt"

# 使用临时文件
touch "$TMPFILE1" "$TMPFILE2"

echo "Temp files created in: $TMPDIR"

模式4:健壮参数解析

#!/bin/bash
set -Eeuo pipefail

# 默认值
VERBOSE=false
DRY_RUN=false
OUTPUT_FILE=""
THREADS=4

usage() {
    cat <<EOF
Usage: $0 [OPTIONS]

Options:
    -v, --verbose       启用详细输出
    -d, --dry-run       运行而不进行更改
    -o, --output FILE   输出文件路径
    -j, --jobs NUM      并行作业数
    -h, --help          显示此帮助消息
EOF
    exit "${1:-0}"
}

# 解析参数
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

# 验证必需参数
[[ -n "$OUTPUT_FILE" ]] || { echo "ERROR: -o/--output is required" >&2; usage 1; }

模式5:结构化日志记录

#!/bin/bash
set -Eeuo pipefail

# 日志记录函数
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 "Starting script"
log_debug "Debug information"
log_warn "Warning message"
log_error "Error occurred"

模式6:带信号的进程编排

#!/bin/bash
set -Eeuo pipefail

# 跟踪后台进程
PIDS=()

cleanup() {
    log_info "Shutting down..."

    # 终止所有后台进程
    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

# 启动后台任务
background_task &
PIDS+=($!)

another_task &
PIDS+=($!)

# 等待所有后台进程
wait

模式7:安全文件操作

#!/bin/bash
set -Eeuo pipefail

# 使用 -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_rmdir() {
    local -r dir="$1"

    if [[ ! -d "$dir" ]]; then
        echo "ERROR: Not a directory: $dir" >&2
        return 1
    fi

    # 使用 -I 标志在rm前提示(BSD/GNU兼容)
    rm -rI -- "$dir"
}

# 原子文件写入
atomic_write() {
    local -r target="$1"
    local -r tmpfile
    tmpfile=$(mktemp) || return 1

    # 先写入临时文件
    cat > "$tmpfile"

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

模式8:幂等脚本设计

#!/bin/bash
set -Eeuo pipefail

# 检查资源是否已存在
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_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_directory "/var/cache/myapp"
ensure_config "/etc/myapp/config" "DEBUG=false"

模式9:安全命令替换

#!/bin/bash
set -Eeuo pipefail

# 使用 $() 而不是反引号
name=$(<"$file")  # 现代,安全地从文件分配变量
output=$(command -v python3)  # 安全获取命令位置

# 带错误检查的命令替换
result=$(command -v node) || {
    log_error "node command not found"
    return 1
}

# 多行处理
mapfile -t lines < <(grep "pattern" "$file")

# NUL安全迭代
while IFS= read -r -d '' file; do
    echo "Processing: $file"
done < <(find /path -type f -print0)

模式10:干运行支持

#!/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

    "$@"
}

# 使用
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"

    # 解析命名参数
    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

    # 验证必需参数
    [[ -n "$input_file" ]] || { echo "ERROR: --input is required" >&2; return 1; }
    [[ -n "$output_dir" ]] || { echo "ERROR: --output is required" >&2; return 1; }
}

依赖检查

#!/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

最佳实践总结

  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 - 跨系统更可预测

资源