Skip to content

防御性 Bash 脚本与自动化运维规范

防御性 Bash 脚本与自动化运维规范

在 Linux 运维、DevOps CI/CD 流水线与日常系统管理中,Shell / Bash 脚本 是粘合各类命令行工具与系统调用的最高效胶水语言。

然而,由于 Bash 历史上极为宽松的语法宽容度(未定义变量默认返回空字符串、某条命令执行失败继续无脑向下执行、管道中间命令报错被静默吞掉),不规范的 Shell 脚本曾多次引发业界著名的“rm -rf /${DEL_DIR}/* 因变量未定义导致清空根目录”的毁灭级灾难。

践行 防御性 Bash 编程 (Defensive Bash Programming) 规范是每个合格工程师的必备素养。


一、防御性四大神装:set -euo pipefail 深度拆解

在任何生产级 Bash 脚本的开头第 2 行,必须显式声明安全约束指令:

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
选项参数 核心行为与防护意义
set -e (errexit) 命令失败即终止。脚本中任何一条命令返回非 0 退出码时,立即中断退出,绝不带着脏状态继续执行后续毁灭性操作。
set -u (nounset) 未定义变量严禁使用。当试图引用一个未声明或未初始化的变量时,立即抛出报错并终止退出,彻底杜绝 rm -rf "$UNSET_VAR/" 的删库惨剧。
set -o pipefail 管道级联错误感知。默认情况下,管道 cmd1 | cmd2 的返回值仅取决于最后一条命令。开启此选项后,只要管道中**任意一条命令失败**,整个管道立即返回失败码。
IFS=$'\n\t' 安全字段分隔符。将内部字段分隔符限定为换行符与制表符,避免在处理包含空格的文件名时被错误拆分为多个参数。

二、资源安全释放与自愈:trap ... EXIT 实战

编写涉及临时文件生成、网络端口占用或进程锁的脚本时,若脚本被用户 Ctrl+C (SIGINT) 中断或中途 set -e 崩溃,很容易遗留脏文件。使用 trap 捕获 EXIT 伪信号 能确保清理逻辑 100% 触发执行:

# 声明安全退出清理函数
cleanup() {
    local exit_code=$?
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] 正在清理临时资源..."
    # 安全删除临时工作目录
    if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR}" ]]; then
        rm -rf "${TMP_DIR}"
    fi
    # 释放文件排他锁
    if [[ -n "${LOCK_FD:-}" ]]; then
        flock -u "${LOCK_FD}" 2>/dev/null || true
    fi
    echo "脚本已安全退出,最终退出码: ${exit_code}"
    exit "${exit_code}"
}

# 注册信号监听:无论正常退出还是中途报错,统一执行 cleanup
trap cleanup EXIT INT TERM

三、生产级工业标准自动化备份脚本范本

以下是一个结构严密、支持标准命令行参数解析 (getopts)、幂等性执行与详细日志打点的企业级数据库自动化备份脚本:

#!/usr/bin/env bash
# ==============================================================================
# Script Name: db_backup_pipeline.sh
# Description: 生产级 PostgreSQL 数据库自动化压缩备份与 S3 上传脚本
# ==============================================================================

set -euo pipefail
IFS=$'\n\t'

# 默认配置
DB_NAME=""
TARGET_DIR="/data/backups"
RETENTION_DAYS=7
LOG_FILE="/var/log/db_backup.log"
TMP_DIR=""

# 日志输出函数
log() {
    local level="$1"
    shift
    local msg="$*"
    local timestamp
    timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
    echo "[${timestamp}] [${level}] ${msg}" | tee -a "${LOG_FILE}"
}

# 帮助文档展示
usage() {
    cat <<EOF
使用方法: $(basename "$0") -d <数据库名> [-o <备份输出目录>] [-r <保留天数>]

选项说明:
  -d    指定需要备份的数据库名称 (必填)
  -o    指定本地备份目录路径 (默认: /data/backups)
  -r    指定历史备份保留天数 (默认: 7 天)
  -h    显示此帮助信息
EOF
    exit 1
}

# 退出清理钩子
cleanup() {
    local code=$?
    if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR}" ]]; then
        rm -rf "${TMP_DIR}"
    fi
    log "INFO" "备份流程执行结束,退出码: ${code}"
    exit "${code}"
}
trap cleanup EXIT INT TERM

# 参数解析
while getopts ":d:o:r:h" opt; do
    case "${opt}" in
        d) DB_NAME="${OPTARG}" ;;
        o) TARGET_DIR="${OPTARG}" ;;
        r) RETENTION_DAYS="${OPTARG}" ;;
        h) usage ;;
        \?) log "ERROR" "未知参数: -${OPTARG}"; usage ;;
        :) log "ERROR" "选项 -${OPTARG} 需要提供参数值"; usage ;;
    esac
done

if [[ -z "${DB_NAME}" ]]; then
    log "ERROR" "必须通过 -d 选项指定数据库名称!"
    usage
fi

# 核心备份执行流程
main() {
    log "INFO" "开始执行数据库 [${DB_NAME}] 自动化备份任务..."

    # 创建安全的临时目录与输出目录
    TMP_DIR="$(mktemp -d /tmp/db_backup_XXXXXX)"
    mkdir -p "${TARGET_DIR}"

    local timestamp
    timestamp="$(date '+%Y%m%d_%H%M%S')"
    local backup_file="${TMP_DIR}/${DB_NAME}_${timestamp}.sql.gz"
    local target_file="${TARGET_DIR}/${DB_NAME}_${timestamp}.sql.gz"

    # 执行导出并流式压缩 (若 pg_dump 失败触发 set -e 自动退出)
    log "INFO" "正在导出数据并执行 gzip 压缩..."
    pg_dump -Fc "${DB_NAME}" | gzip -c > "${backup_file}"

    # 校验文件大小
    local file_size
    file_size="$(du -h "${backup_file}" | awk '{print $1}')"
    log "INFO" "数据导出完成,压缩包体积: ${file_size}"

    # 移动至目标存储目录
    mv "${backup_file}" "${target_file}"
    log "SUCCESS" "备份成功持久化至: ${target_file}"

    # 清理过期旧备份 (幂等性清理)
    log "INFO" "清理超过 ${RETENTION_DAYS} 天的历史旧备份..."
    find "${TARGET_DIR}" -name "${DB_NAME}_*.sql.gz" -mtime +"${RETENTION_DAYS}" -exec rm -f {} +

    log "SUCCESS" "数据库 [${DB_NAME}] 备份与轮换任务圆满完成!"
}

main

四、防御性编码五大黄金戒律

  1. 所有的变量展开必须用双引号包裹:严格使用 "${MY_VAR}" 而非裸 $MY_VAR,防止包含空格或特殊字符引发词法解析分裂。
  2. 条件测试严格使用双中括号 [[ ... ]]:相比单括号 [ ... ][[ ... ]] 支持正规逻辑与正则表达式,且不会在变量为空时产生语法报错。
  3. 在危险删除操作前强制校验变量非空
    # 安全的删除写法
    rm -rf "${TARGET_DIR:?TARGET_DIR 变量未定义,拒绝删除!}"/*
    
  4. 引入 ShellCheck 静态代码检查:在 CI 流水线中集成 shellcheck myscript.sh,在提交前自动扫描潜在的陷阱与语法缺陷。