掌握Bash脚本调试:开发者必备技巧

通过语法检查、xtrace、严格模式、陷阱、ShellCheck和精准日志调试Bash脚本。

掌握Bash脚本调试:开发者必备技巧

Bash脚本调试从一个问题开始:脚本在哪里做了与你预期不同的事情?良好的调试能让你看到语法错误、变量展开、命令顺序和退出状态,而不会让脚本变得杂乱无章。

本指南将介绍实用的Bash调试技巧,你可以在实际自动化脚本进入cron、CI或生产环境之前使用它们。

从语法检查开始

在追踪运行时行为之前,确保Bash能解析文件:

bash -n ./deploy.sh

bash -n 读取脚本并报告语法错误,但不运行命令。它能捕获缺失的 fidonethen、引号和花括号。它不会捕获逻辑错误、缺失文件或运行时失败的命令。

例如,这个拼写错误会在任何代码运行前被捕获:

if [ -f "$CONFIG" ]; then
    echo "Config found"
# 缺少 fi

在大规模编辑后以及添加更多调试输出之前,运行语法检查。

使用 set -x 追踪执行

最有用的内置调试器是xtrace:

set -x
some_command "$VALUE"
set +x

启用追踪后,Bash会在展开后、执行前打印每个命令。这有助于你看到变量是否为空、通配符是否展开、或命令是否收到了预期之外的参数。

对于整个脚本的追踪,运行:

bash -x ./deploy.sh

为了更清晰的追踪,设置 PS4 使每行包含源文件行号:

export PS4='+ ${BASH_SOURCE}:${LINENO}: '
bash -x ./deploy.sh

如果你的脚本处理机密信息,不要追踪打印令牌、密码或签名URL的部分。在这些命令之前关闭追踪:

set +x
login_with_secret "$API_TOKEN"
set -x

谨慎添加严格模式

这些选项能更早地捕获常见失败:

set -euo pipefail

set -e 在许多未处理的命令失败时退出。set -u 将未设置的变量视为错误。set -o pipefail 使管道中任何一个命令失败时整个管道失败,而不仅仅是最后一个命令。

它们很有用,但不能替代显式处理。像 grep 这样的命令可能因正常的“未找到”结果返回 1

if grep -q "READY" status.txt; then
    echo "ready"
else
    echo "not ready"
fi

这比用 grep -q "READY" status.txt || true 隐藏结果更清晰。

打印正确的值

精准日志胜过散乱的 echo 行。打印影响你正在调试的分支的值:

printf 'DEBUG: user=%q env=%q target=%q\n' "$USER_NAME" "$ENVIRONMENT" "$TARGET_HOST" >&2

printf '%q' 显示shell转义后的值,使空格和特殊字符更容易发现。将调试输出发送到stderr,以便正常脚本输出在管道中仍然可用。

当命令失败时,立即捕获其状态:

run_migration
status=$?

if [ "$status" -ne 0 ]; then
    echo "Migration failed with exit code $status" >&2
    exit "$status"
fi

在保存 $? 之前不要运行其他命令,因为即使是 echo 也会替换它。

调试循环和条件语句

循环错误通常来自单词分割或意外输入。引用变量并安全地读取行:

while IFS= read -r line; do
    printf 'line=%q\n' "$line" >&2
done < input.txt

对于条件语句,打印正在比较的确切值:

printf 'expected=%q actual=%q\n' "$EXPECTED" "$ACTUAL" >&2

if [[ "$ACTUAL" == "$EXPECTED" ]]; then
    echo "match"
fi

如果在本地调试时需要暂停脚本,read 可以工作:

read -r -p "Press Enter to continue..."

在提交脚本前移除暂停,特别是如果它可能无人值守运行。

使用ShellCheck进行静态分析

ShellCheck能捕获许多Bash会愉快运行直到在边界情况中崩溃的问题:

shellcheck ./deploy.sh

它会标记未引用的变量、不可达代码、可疑测试、未使用变量和可移植性问题。将警告视为检查代码的提示,而不是自动证明脚本有误。有时你可能有意禁用某个警告,但应添加简短注释解释原因。

使用 trap 查看失败行

对于较长的脚本,错误陷阱可以告诉你失败发生的位置:

set -Eeo pipefail

trap 'echo "Error on line $LINENO: $BASH_COMMAND" >&2' ERR

set -E 帮助 ERR 陷阱在Bash的函数和子shell中传播。这在CI日志中很有用,因为你可能没有交互式shell。

要点

bash -n 开始,使用 bash -x 或针对性的 set -x 进行运行时追踪,并在行为不正确的分支周围添加精准的stderr日志。对于重要的脚本,运行ShellCheck并添加 ERR 陷阱,以便失败指向需要关注的命令和行。