
运维服务器时,很多重复性工作都靠 Bash 脚本自动完成:备份数据库、清理日志、同步文件、部署代码。脚本跑得顺时很省心,可一旦中途出错,问题往往比手动操作更麻烦——备份只写了一半、临时文件没删、进程没停干净,下次再跑就会叠加出新的故障。这篇指南会教你用 Bash 的 trap 和 set -e 给自动化任务加上可靠的容错逻辑,让脚本在出错时能主动清理现场、记录原因,而不是带着半成品状态悄悄退出。无论你用的是 VPS(虚拟专用服务器)还是独立服务器,这套方法都适用,相关的服务器配置与优化经验也可以一并参考。
为什么脚本会”悄悄失败”
很多新手写的脚本默认”出错也继续跑”。Bash 里一条命令返回非零退出码,脚本并不会自动停止,而是接着执行下一条。比如备份脚本里 tar 因为磁盘空间不足失败了,后面的 rm 却照常把旧备份删掉,结果就是既没备份成功,又把能用的旧文件清空了。这种”静默失败”在无人值守的定时任务里尤其危险,因为没人盯着输出,等发现问题时数据可能已经丢了。
要改变这个默认行为,最直接的办法是在脚本开头加上 set -e。它会让脚本在任意一条命令返回非零时立即退出,避免错误被后续命令掩盖。配合 set -u(变量未定义时报错)和 set -o pipefail(管道中任一段失败都算整体失败),能覆盖绝大多数”悄悄失败”的场景。

trap 的作用:出错时也要善后
set -e 解决了”出错要停”,但没解决”停了之后怎么办”。脚本退出前,可能还留着临时目录、锁文件、未提交的进程。这时候就需要 trap。trap 可以绑定一个信号或事件,在脚本退出、收到中断信号或出错时执行指定的清理函数。
用 trap 清理临时文件
最常见的用法是清理临时目录。下面这段脚本先建一个临时目录,再在退出时把它删掉:
#!/bin/bash
set -euo pipefail
TMPDIR=$(mktemp -d)
cleanup() {
rm -rf "$TMPDIR"
echo "已清理临时目录 $TMPDIR"
}
trap cleanup EXIT
# 业务逻辑...
trap cleanup EXIT 表示无论脚本正常结束还是出错退出,都会先执行 cleanup。这样即使中间某条命令失败,临时文件也不会残留。

区分正常退出与异常退出
如果希望出错时做不同的处理,可以给 trap 绑定 ERR 信号。ERR 只在命令返回非零时触发,正常退出不会执行:
#!/bin/bash
set -euo pipefail
err_handler() {
echo "脚本在第 $LINENO 行出错,退出码 $?" >&2
}
trap err_handler ERR
把 $LINENO 和 $? 写进日志,能帮你快速定位是哪一行、哪个命令出了问题,这在排查无人值守任务时非常有用。
一个完整的备份脚本示例
把上面的技巧组合起来,就是一个可用的备份脚本。它先检查磁盘空间,再打包数据库目录,出错时记录日志并清理临时文件:
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/backup/mysql"
TMPDIR=$(mktemp -d)
LOG="/var/log/backup.log"
cleanup() {
rm -rf "$TMPDIR"
}
err_handler() {
echo "$(date '+%F %T') 备份失败,第 $LINENO 行,退出码 $?" >> "$LOG"
cleanup
}
trap cleanup EXIT
trap err_handler ERR
# 检查磁盘空间,低于 2GB 直接退出
avail=$(df --output=avail "$BACKUP_DIR" | tail -1)
if [ "$avail" -lt 2097152 ]; then
echo "磁盘空间不足,剩余 $((avail/1024/1024))MB" >&2
exit 1
fi
tar -czf "$TMPDIR/mysql-$(date +%F).tar.gz" /var/lib/mysql
mv "$TMPDIR/mysql-$(date +%F).tar.gz" "$BACKUP_DIR/"
echo "$(date '+%F %T') 备份完成" >> "$LOG"
这段脚本里,df 检查磁盘空间、tar 打包、mv 移动文件,任何一步失败都会触发 err_handler 记录日志并清理临时目录,不会留下半成品。
处理中断信号与超时
定时任务还可能被手动中断,或者因为网络问题卡住。trap 同样可以捕获 INT(Ctrl+C)和 TERM(kill 默认信号),让脚本在被打断时也能收尾:
#!/bin/bash
set -euo pipefail
stop_handler() {
echo "收到中断信号,正在停止..." >&2
kill 0 2>/dev/null || true
exit 1
}
trap stop_handler INT TERM
kill 0 会向当前进程组的所有进程发送信号,适合在脚本里启动了多个子进程时统一收尾。加上 || true 是为了避免没有子进程时报错。
对于可能卡死的网络操作,可以给命令加超时。timeout 命令能限制单条命令的运行时长,比如 timeout 60 curl -sS https://example.com/api 最多等 60 秒,超时返回 124,配合 set -e 就会触发错误处理。
错误处理与日志的配合
无人值守的脚本最好把关键信息写进日志,而不是只输出到终端。上面示例里用 >> "$LOG" 追加写入,配合 date 记录时间戳,方便事后排查。你也可以把日志路径作为变量统一管理,避免散落在脚本各处。
日志要区分正常信息和错误信息。正常进度用 echo 输出,错误信息用 echo ... >&2 写到标准错误,这样重定向时能分开处理。比如 ./backup.sh 2>>error.log 只把错误写进单独的文件,正常输出留给终端。如果你在跑 WordPress 站点的定时备份,这类日志配合WordPress 教程里的维护经验,能更系统地管理站点健康。

常见坑与避坑建议
set -e 有几个容易踩的坑。第一,if 条件里的命令即使返回非零也不会触发退出,因为 if 本身依赖退出码做判断,这是正常行为,不用改。第二,管道命令要配合 set -o pipefail,否则只看最后一段的退出码,前面的失败会被忽略。第三,grep 没匹配到内容时返回 1,如果脚本里用 grep 做存在性检查,要放在 if 或 || true 里,否则会被 set -e 误杀。
建议在写脚本时先想清楚”哪些错误必须停、哪些可以忽略”。set -e 是全局开关,但个别命令可以用 || true 或 if 显式声明”这里失败没关系”。这样既保留了全局的严格性,又不会因为个别可容忍的失败让整个任务中断。
总结
给服务器自动化任务加容错,核心就三件事:用 set -euo pipefail 让脚本出错即停,用 trap 在退出时清理现场、记录原因,用日志和超时让无人值守的任务可追踪、不卡死。这套组合能覆盖绝大多数备份、同步、部署脚本的失败场景,避免”半成品状态”累积成更大的故障。如果你需要一台稳定、可随时跑这类自动化任务的服务器,可以考虑 Hostease 的 VPS 主机或独立服务器方案,它们都提供完整的 root 权限,方便你按自己的方式配置 cron 定时任务和错误处理逻辑。建议先从一个小脚本开始,把 trap 和 set -e 用熟,再逐步推广到生产环境的自动化任务上。