脚本行为不对,第一反应是加 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。

相关工具与速查