西西软件园多重安全检测下载网站、值得信赖的软件下载站!
软件
软件
文章
搜索

首页 → 业内动态 → 业内资讯 → 部署脚本跑了 40 分钟才失败:set -e 为什么没拦住?

部署脚本跑了 40 分钟才失败:set -e 为什么没拦住?

相关软件相关文章发表评论 来源:本站整理时间:2026/11/4 4:41:01字体大小:A-A+

作者:xiix点击:0次评论:0次标签: shell脚本 set-e pipefail 严格模式 部署脚本

  • 类型:安卓其它大小:29.0M语言:中文 评分:10.0
  • 标签:
立即下载

上个月一个周日凌晨两点,我盯着屏幕看一条发布脚本跑。前半段一切正常:拉代码、打镜像、推仓库、滚动重启,每一步都打了绿色的对勾,眼看快收尾了,结果脚本在最后一行吐出一句 tar: unexpected EOF,退出码非零,整条流水线被判失败。那次发布一共耗时 40 分钟,我第一反应是「磁盘是不是满了」,第二反应是「镜像是不是拉坏了」,第三反应才是把脚本从头翻了一遍问题出在最开头那句 set -e 上,它没拦住真正出错的那一行,反倒是把后面无关紧要的检查给拦了下来。

如果你手上也是一台自己管的云服务器,准备写点 shell 脚本做备份、发布或者定时任务,先看看这几家的机器和方案再动手:md5host(VPS / 服务器 / 域名,免实名开通),脚本本身没什么门槛,机器稳不稳、线路靠不靠谱才是长期影响你睡眠质量的部分。想多开几台做压测或者做灰度环境的话,NiiHost(服务器与主机) 也可以放进对比清单里,配置和价格自己按项目规模挑就行。

一、set -e 到底做了什么

set -e 的语义一句话就能说清:脚本执行过程中,只要某条命令的退出码非零,脚本立刻退出。这句话听起来很美,实际用起来坑多得离谱,因为它有相当长一串「不生效」的例外场景。

第一,命令出现在 if 的条件判断里,不生效。if grep -q "error" /var/log/app.log; then ... 这种写法是故意的,grep 没匹配到返回 1 是正常结果,不该把脚本干掉。第二,命令出现在 && 或 || 组成的链路里,只有整条链路的最后一个命令决定退出码。第三,命令前面加了 ! 取反,也不生效。第四,命令在 $( ) 命令替换里,退出码会被整个赋值语句吃掉,除非你写 VAR=$(cmd) || exit 1。第五,也是最容易中招的一条:命令处在管道里,而 pipefail 没开。

Linux 管道命令流程可视化

二、真正的元凶是管道

先说管道的工作方式。一条 docker build ... | tee build.log | grep -i error 这样的命令,bash 会把它拆成三个子进程并行跑,最后取最右边那个命令的退出码当作整条管道的退出码。这条规则本身没毛病,但和 set -e 撞在一起就成了事故现场。

举个我踩过的真实例子:备份脚本里写着 tar -czf - /data | gzip > backup.tgz,磁盘写到 87% 时 tar 报错了,但 gzip 拿到的是一段正常的结束流,gzip 退出码是 0,于是 set -e 认定整条命令成功,脚本继续往下跑,最后还把「备份成功」记进了日志。等到需要恢复的那天,我才发现这个压缩包只有一半数据。同理,curl -s url | jq . 里 curl 因为超时返回了 28,jq 解析空输入返回 0,错误也被吞得干干净净。

解法有两个。第一个是开 set -o pipefail,这样管道里任意一环失败,整条管道的退出码就变成非零,set -e 立刻生效。第二个是在关键位置显式检查:tar -czf - /data | gzip > backup.tgz || { echo "备份失败" >&2; exit 1; }。如果你只是想把某个环节的错误抓出来又想继续往下走,可以用 ${PIPESTATUS[@]},它保存了管道里每个命令的退出码,echo "${PIPESTATUS[@]}" 就能一次看到全部结果。

运维工程师在终端前排查脚本报错

三、严格模式的三个开关,我一般是这么写

真正让我省心的写法是三个一起上,而且必须放在脚本最前面:

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

-e 就是刚才说的遇错即停。-u 表示引用未定义变量时直接报错,这个开关的价值被严重低估很多脚本拿 $1 取参数却不判断个数,少传一个参数时 $1 是空的,脚本不报错而是拿着空字符串去拼路径,最后删错了目录;开了 -u 之后,缺参数会在第一秒就抛错退出。-o pipefail 则是刚才那条管道的解药。

两个细节要提醒:一是 shebang 必须写 bash 而不是 sh,因为 pipefail 和数组 PIPESTATUS 都是 bash 的特性,很多精简版 busybox 环境的 sh 根本不认;二是这三个开关要写在脚本第一行之后、任何业务逻辑之前。如果你在脚本中间才写上 set -euo pipefail,前面已经执行过的命令可不会补判。

定时任务互斥锁的等距插画

四、错误被吃掉的其他几种姿势

除了管道,还有几个地方会让 set -e 白写。

最典型的是命令替换:VERSION=$(curl -s https://api.example.com/version),如果 curl 失败,$VERSION 拿到的是空字符串,脚本继续拿它去拼版本号,等到用户报「版本显示为 0」你才会反应过来。正确写法是 VERSION=$(curl -fsSL https://api.example.com/version) || { echo "取版本号失败" >&2; exit 1; },注意 curl 加 -f 参数,HTTP 错误码也会变成非零退出。

第二个是「检查类」命令误伤。grep 没找到就是退出码 1,diff 发现差异也是 1,这在 set -e 下是致命的。解决办法是显式处理:grep -q "error" logfile || true,或者用 if ! grep -q ...; then 包起来。这两种写法的区别是:前者明确表示「这个失败我允许」,后者表示「失败就走另一条分支」,后者语义更清楚。

第三个是函数返回值。bash 函数的退出码是它内部最后一条命令的退出码,所以如果函数最后一行是 echo,那这个函数永远返回 0,前面哪怕已经出错也不会被 set -e 抓住。稳妥的做法是在函数结尾显式 return 0 或者把关键检查放在中间并直接 return 1。

五、失败之后还得有人收拾烂摊子

脚本中途退出的时候,临时文件、锁文件、临时容器这些东西可能还留在磁盘上。手动清理靠不住,得靠 trap。写法很简单:

cleanup() { rm -f "$TMP_DIR"/work.log; docker rm -f "$CID" 2>/dev/null || true; }
trap cleanup EXIT

trap ... EXIT 会在脚本以任何方式退出时触发,包括正常结束、报错退出和被 Ctrl+C 打断(如果同时注册了 INT 和 TERM 会更完整)。清理动作本身也要小心,它是在 set -e 生效的状态下跑的,所以里面那些「可能不存在」的删除命令记得加 || true,否则清理失败会覆盖掉真正的错误信息,这正是我第一次写 trap 时干过的蠢事原始报错被清理报错盖住了,日志里只剩一句无意义的 rm: cannot remove。

脚本逐行执行流程扁平插画

六、放进 cron 和 systemd 里,又是另一套坑

脚本在终端里跑得好好的,扔进 crontab 就各种玄学,常见的三个原因是环境变量和终端不一样、PATH 里没有 /usr/local/bin,以及默认用 /bin/sh 执行导致 set -o pipefail 直接报错。解决办法是在 crontab 那一行显式指定解释器:30 3 * * * /bin/bash /opt/scripts/backup.sh >> /var/log/backup.log 2>&1,脚本内部再补一句 export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin。用 systemd 定时器的话则相对干净,但同样要记得给服务单元写上 Environment=PATH=... 和 WorkingDirectory=。

最后留一份我自己在用的骨架,你可以直接抄走改:

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

log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; }

main() {
log "开始备份"
tar -czf - /data | gzip > /backup/data.tgz
log "备份完成"
}

trap 'log "脚本异常退出"' ERR
main "$@"

把这套东西养成习惯之后,我上一个跑了 40 分钟才失败的发布脚本,现在通常在第 7 秒就停下来了,而且日志里能直接看到是哪一环、哪个管道成员的退出码不对。脚本这东西,写的时候多花两分钟加三行开关,能省掉以后无数个凌晨两点的排查时间。

    相关评论

    阅读本文后您有什么感想? 已有人给出评价!

    • 8 喜欢喜欢
    • 3 顶顶
    • 1 难过难过
    • 5 囧囧
    • 3 围观围观
    • 2 无聊无聊

    发表评论

    昵称:
    表情: 高兴 可 汗 我不要 害羞 好 下下下 送花 屎 亲亲