技术文章2026 年 8 月 21 日约 3.6 分钟

Shell 运维脚本基础:严格模式与安全参数处理

用 set -Eeuo pipefail、参数校验、trap 清理与预览确认,写出半夜出事也不会闯祸的运维脚本。

运维脚本大多在凌晨跑、在故障时跑、在没人盯着的时候跑。一个脚本最可怕的结局不是报错,而是半路出错却继续执行:备份只做了一半、文件被挪了一半、服务重启了一半。这篇文章讲怎么写一个「要么全成,要么明确失败」的 Shell 脚本。

#严格模式:set -Eeuo pipefail 的每一段都要懂

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

四个开关各管一件事,缺一个都有漏洞:

  • -e:命令返回非零就退出。没有它,脚本会「干到底」,前面的失败全被后面的成功掩盖。
  • -u:引用未定义变量就报错退出。没有它,$未定义变量 是空字符串,rm -rf $DIR/ 可能变成 rm -rf /。
  • -o pipefail:管道中任何一个命令失败,整条管道就算失败。没有它,mysqldump xxx | gzip > backup.sql.gz 里 mysqldump 失败时,gzip 可能照常成功,你得到的是一个空备份文件,还显示「成功」。
  • -E:让 trap ERR 能捕获到子函数和子 shell 里的错误,配 trap 'echo "line $LINENO failed"' ERR 能定位失败行。

最常见的反模式是脚本里写着 set -e,但到处用 cmd || true 把错误吞掉。要么明确接受失败并处理,要么就让它炸出来——|| true 是给「失败也无所谓」的命令用的,不是给错误处理偷懒用的。

#参数校验:先拒后行

严格模式只解决「中途出错」,参数错了要在一开始就拦住:

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

usage() {
    echo "用法: $0 <目录>" >&2
    exit 2
}

[[ $# -eq 1 ]] || usage
work_dir=$1

# 目录必须存在、必须是目录、必须可读
[[ -d "$work_dir" ]] || { echo "错误: 不是目录: $work_dir" >&2; exit 2; }
[[ -r "$work_dir" ]] || { echo "错误: 目录不可读: $work_dir" >&2; exit 2; }

注意变量永远加双引号:"$work_dir"。不加引号,文件名带空格时会被拆成多个参数,rm -rf $work_dir/ 直接拆成两条命令——这是脚本误删的头号原因。

#临时文件和清理:trap EXIT

脚本创建临时文件,失败退出时会留下垃圾;更糟的是下次运行读到残留文件。用 trap ... EXIT 保证无论正常还是异常退出都清理:

bash
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT

# 之后脚本任何位置失败退出,tmp_dir 都会被清掉

mktemp -d 生成的目录名带随机后缀,比手写 /tmp/myjob_$$ 安全(避免符号链接攻击和路径冲突)。绝对不要用固定的 /tmp/xxx 路径,多实例并发跑同一个脚本时会互相踩。

#危险操作:预览 + 确认开关

涉及删除、重启、发布的操作,脚本默认只打印动作,加 --apply(或 -y)才真执行。我在生产脚本里常用的写法:

bash
apply=false
[[ "${1:-}" == "--apply" ]] && apply=true

do_backup() {
    echo "[action] 备份 $src -> $dst"
    if $apply; then
        cp -a "$src" "$dst"
    fi
}

do_backup

输出里用 [action] 前缀标记将要发生的操作,人类看日志一眼就能核对。删除类操作再额外加一道确认:

bash
if $apply; then
    read -r -p "确认删除 $target 下的过期备份? [y/N] " ans
    [[ "$ans" == "y" ]] || { echo "已取消"; exit 0; }
fi

#日志:打清楚、不打敏感信息

脚本要能在事后复盘,日志至少要包含:时间、操作对象、结果。用 set -x 调试时尤其小心——它会把命令行原样打出来,export PASSWORD=xxx 这种就会泄露。

bash
log() { printf '%s %s
' "$(date '+%F %T')" "$*"; }
log "开始同步 $work_dir"
# ...
log "同步完成,共 ${count} 个文件"

我在一个事故里见过:脚本用 set -x 跑,把带数据库密码的连接串打进了 CI 日志,日志又同步到第三方平台。含密码的命令永远不要出现在 set -x 的输出里,敏感参数从环境变量读,且只在需要时导出。

#发布前检查清单

写完脚本,bash -n 只查语法,shellcheck 能查出一堆真问题:

bash
bash -n ops.sh
shellcheck ops.sh

ShellCheck 的警告里,SC2086(变量没加引号)、SC2164(cd 失败没处理)、SC2012(ls 解析)都是真实事故的高发项。我的经验是:任何超过 50 行的运维脚本都值得过一遍 shellcheck,它抓出来的问题远比「再小心一点」管用。

最后总结成一句话:set -Eeuo pipefail + 双引号 + trap 清理 + 预览确认,这四样配齐,脚本的「半夜出事率」能降一个量级。

写于 2026 年 8 月 21 日

栏目
技术文章
约
3.6 分钟
字数
2.2K
阅读
241

本文为原创记录,转载请注明出处。如果这篇替你省了时间,欢迎留言说说你踩到的坑。

同题 · related

留言 · remarks

00 条

还没有留言,来说点什么吧。

Shell 运维脚本基础:严格模式与安全参数处理 · LXH·BLOG