Shell 脚本的健壮性:set -euo pipefail 治不了的病

举报
yd_238022924 发表于 2026/11/07 00:21:51 2026/11/07
【摘要】 从 set -euo pipefail 三个开关讲起,梳理条件语句、子 shell、trap 收尾里那些它管不到的地方。

Shell 脚本的健壮性:set -euo pipefail 治不了的病

默认的 bash 是个乐天派

写脚本时最容易犯的错,是以为出错就会停。实际上 bash 的默认行为是:一条命令失败了,它接着往下跑。

于是就有了这种事故——备份脚本 cd /some/dir 失败(目录不存在),后面一行 rm -rf ./* 就在错误的目录里执行了。没有任何报错,脚本退出码还是 0。

所以稍微正经一点的脚本,开头都会写这行:

set -euo pipefail

这三个开关各自管一件事,很多人抄了但没弄清分别管什么。

三个开关分别治什么

开关 作用 治的病
-e 任何命令返回非 0 就立即退出 出错继续往下跑
-u 引用未定义变量就报错退出 $DIR/* 里 $DIR 忘了定义,变成 /*
pipefail 管道里任一环节失败,整条管道算失败 a | b 只看 b 的退出码

-u 这条被低估得最厉害。rm -rf "$BUILD_DIR"/* 里如果 $BUILD_DIR 因为拼写错误而为空,在没开 -u 的情况下就变成了 rm -rf /*。这不是段子,是真事。

它治不了的几种病

一、条件语句里的失败不会退出。

set -e
if grep -q pattern file; then echo "found"; fi
echo "继续执行"

grep 没匹配到会返回 1,但因为它在 if 的条件位置上,-e 不会触发。这是有意设计的,不是 bug——但写的时候要知道。

二、子 shell 和命令替换。

set -e
result=$(false; echo "still here")
echo "$result"

$(...) 里跑的是子 shell,整体退出码取最后一条命令。默认不当成失败,-e 也就没反应。要抓这种情况,除了 pipefail,还得在函数里自己检查返回值。

三、trap 才是真正的收尾手段。

set -e 只负责"停",不负责"清理"。脚本中途退出时,临时文件、锁文件、半成品都得自己收。

#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT
trap 'echo "[失败] 第 $LINENO 行: $BASH_COMMAND" >&2' ERR

这里的 set -E 常被忽略:它让 ERR trap 能穿透函数和子 shell。少了它,函数内部的失败不会触发 trap,出错时你只能看到一个光秃秃的退出码。

四、kill -9 谁也管不了。

trap ... EXIT 在进程被 SIGKILL 时不会执行。所以别把"数据一致性"全押在 trap 上;关键状态要能自愈,比如启动时检查遗留的锁文件、用临时文件加 mv 做原子替换。

顺手一个 safeguards:${VAR:?}

set -u 只能挡住"变量没定义",挡不住"定义了但是空字符串"。而危险操作里更常见的恰恰是后者——配置项读到了,但内容是空的。

rm -rf "${BUILD_DIR:?}/"*

:? 的意思是"为空或未定义就立刻报错退出,并打印后面那句话"。上面的写法在 BUILD_DIR 为空时直接中止,不会退化成 rm -rf /。凡是拿变量拼路径再交给 rm、mv 的地方,都值得加这个后缀。

出错信息比退出码更有用

set -e 只给你一个退出码,出问题时你还是得回去翻脚本。加一行 ERR trap,把行号和正在执行的命令打出来:

trap 'echo "[失败] 第 $LINENO 行: $BASH_COMMAND" >&2' ERR

配合 set -x(只在调试时开)和 bash -x script.sh,定位成本能从"猜半小时"降到"看一眼"。

一个能直接用的开头

#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

IFS 改成换行和制表符,是为了让 for f in $list 这种写法不会因为文件名里有空格而拆错。这是最后一条容易被漏掉、但代价很大的设置。

上线前自检

  1. 用 bash -n script.sh 检查语法,用 shellcheck 找隐患;
  2. 拿空目录、不存在的路径、无权限的文件各跑一次,看它是"明确报错退出"还是"静默继续";
  3. 在 trap EXIT 里打印实际执行到了哪一步,出事时能定位;
  4. 危险操作(rm -rf、mv 覆盖)前打印目标路径并 read -r 确认,或者至少在日志里留痕。

脚本写得好不好,不体现在顺风时跑得多快,而体现在出错时它停不停、停得干不干净。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。