脚本行为不对,第一反应是加 set -x 看执行过程——然后满屏都是 + 开头的输出,关键那行反而被淹没了。更糟的是忘了关,调试信息混进生产日志,日志量直接翻倍。
这篇文章把 Bash 调试的 10 个实用技巧按「快速定位 → 控制输出 → 防患未然」的顺序排好,重点解决一个问题:怎么在需要的时候才看到调试输出,不需要的时候干干净净。
先回答标题:set -x 怎么关掉
set -x # 开启:之后每条命令执行前都打印出来
set +x # 关闭:注意是 + 不是 -
反直觉的点:bash 的 set 是 - 开启、+ 关闭,跟直觉正好相反(shopt -s/-u 又是另一套,别混)。所以「关掉 set -x」的答案是 set +x,不是 set -x off 之类的写法。
招 1:局部开关,别全局开
最常见的错误是在脚本开头 set -x 就不管了。正确做法是只包住可疑的那几行:
#!/bin/bash
# 前面的正常逻辑不追
set -x
critical_cmd --foo "$var"
result=$?
set +x
# 后面恢复干净输出
招 2:bash -x 整脚本追,不改代码
临时排查时不要往脚本里写死 set -x(写了就一定会忘删):
bash -x deploy.sh # 只在本次执行时追踪
bash -x deploy.sh 2>debug.log # trace 走 stderr,重定向到文件
招 3:用 BASH_XTRACEFD 把 trace 单独甩到文件
2>debug.log 会把报错信息也混进去。bash 4.1+ 可以给 trace 一个专属文件描述符:
exec {BASH_XTRACEFD}>debug.log # xtrace 单独走这个文件
set -x
# ...正常输出还在终端,trace 全进 debug.log
set +x
终端看业务输出,文件看执行过程,互不干扰。
招 4:PS4 给 trace 加行号和函数名
默认 trace 前缀是 +,看不出是哪一行。改 PS4:
export PS4='+$LINENO ${FUNCNAME[0]}: '
set -x
输出变成 +42 main: cp a b,直接告诉你第 42 行、main 函数里执行了什么。
招 5:set -euo pipefail 三件套
放在脚本开头,很多 bug 根本不用调:
set -euo pipefail
-e:任何命令失败立即退出,不带病运行-u:用了未定义的变量直接报错,不再当空字符串pipefail:a | b | c中任意一环失败,整体算失败(默认只看最后一环)
注意 -e 的例外:if 条件里、&&/|| 左侧的命令失败不会触发退出,这是设计如此。
招 6:trap 抓住出错那一行
-e 退出了,但不知道死在哪儿?加一行:
trap 'echo "出错了:行 $LINENO,命令 $BASH_COMMAND,退出码 $?" >&2' ERR
脚本一失败就打印行号、原始命令和退出码,比翻 trace 快得多。
招 7:bash -n 先查语法
bash -n deploy.sh # 只检查语法,不执行
部署前、CI 里都先跑一遍,语法错误根本不该留到运行时。
招 8:日志带时间戳和级别
别用裸 echo,包一层:
log() { echo "[$(date '+%F %T')] [$1] $2"; }
log INFO "开始部署"
log ERROR "连接失败"
以后 grep ERROR 一把捞出来,还能按时间排序对齐多台机器的日志。
招 9:shellcheck 静态检查
shellcheck deploy.sh
没加引号的变量、被覆盖的 $?、cat file | grep 这类问题,shellcheck 全能在执行前抓出来。它比 set -x 更早发现问题,应该是写完脚本的第一步。
招 10:给破坏性操作加 --dry-run
DRY=1
run() {
if [ "$DRY" = 1 ]; then echo "[dry-run] $*"; else "$@"; fi
}
run rm -rf "$tmpdir"
先演练一遍看会执行什么,确认无误再真跑,删库跑路式事故少一半。
推荐工作流
写完 → shellcheck → bash -n → 加 set -euo pipefail 跑
→ 出问题:局部 set -x + PS4 定位 → 修好
→ 线上:留 log(),不留 set -x
常见坑
set -x写在 crontab 调用的脚本里忘了删:定时任务每次跑都产生一大段 trace,邮件或日志被塞满。定时脚本更不该留set -x,排查时先看 cron 日志确认脚本到底跑没跑。set -e后脚本"莫名"退出:多半是某条命令返回非零被你忽略了,配合 trap 能看到真凶。- trace 里变量已展开:
set -x打印的是展开后的命令,想知道变量原值,在可疑行前加一行declare -p varname。
相关工具与速查
- Bash 命令速查:本文涉及的 set / trap / exec / PS4 等用法速查
- crontab 命令速查:定时任务脚本调试,先看日志输出配置
- Cron 表达式在线解析:脚本没问题但执行时间不对?先确认表达式
- 旧文 Cron 表达式在线解析:6 段语法与常见坑:定时脚本「没按预期跑」时对照排查