49·Shell 脚本进阶进阶

Shell 脚本调试技巧

debugbashdb

第49课:Shell 脚本调试技巧

所属模块: Shell 脚本进阶 难度: Intermediate 标签: debug, bashdb 上一课: trap 信号处理 下一课: 实用脚本编写实战


学习目标

学完本课,你将能够:

  1. 熟练使用 set 命令系列(如 -x, -v)对脚本进行快速跟踪和输出调试。
  2. 掌握利用 bashdb 这一交互式调试器,对复杂脚本进行设置断点、单步执行和变量检查。
  3. 学会结合 echo 和日志输出进行关键位置的状态监控。
  4. 能够高效地定位并修复脚本中的逻辑错误和运行时错误。

核心概念

调试脚本就像是侦探破案,你需要找到“犯罪现场”(出错的位置)和“犯罪动机”(错误的原因)。Shell 提供了从简易“听诊器”到专业“分析仪器”的一系列工具。

1. 快速诊断:set 命令系列

这是最常用、最直接的调试方法,如同给脚本装上“透视镜”。

  • set -x (Trace模式):这是最核心的调试选项。执行此命令后,Shell 会在执行每一条命令之前,先打印出该命令及其展开后的参数。它能让你清晰地看到脚本的“思考过程”。
  • set -v (Verbose模式):在脚本被读取时打印每一行代码。与 -x 的区别在于,-v 打印的是原始代码,而 -x 打印的是经过变量替换和命令展开后的结果。
  • set -e (Exit on error):让脚本在任何命令返回非零状态(通常表示错误)时立即退出。注意:这个选项在调试时可能掩盖错误后续的影响,建议谨慎使用或在关键部分关闭。
  • set -u (Treat unset variables as errors):当脚本引用未定义的变量时,报错并退出。这是防止“变量名拼写错误”这类低级错误的利器。

你可以通过 set -xv 组合使用,获得更详细的输出。调试结束后,用 set +x 关闭追踪。

2. 专业工具:交互式调试器 bashdb

当脚本逻辑复杂,set -x 的输出信息像洪水一样淹没你时,就需要一个可以“暂停、观察、继续”的交互式工具。bashdb 就是 GDB 的 Bash 版本,功能强大。

核心操作:

  • 启动bashdb ./your_script.sh [args]
  • 常用命令
    • n (next): 单步执行下一行,不进入函数。
    • s (step): 单步执行,如果当前行是函数调用,则进入函数内部。
    • c (continue): 继续运行直到下一个断点。
    • b <行号> (break): 在指定行设置断点。
    • p <变量名> (print): 打印变量的值。
    • w (watch): 设置监视点,当表达式值变化时暂停。
    • q (quit): 退出调试器。
    • l (list): 列出当前源代码。

3. 经典方法:echo 与日志

在代码的关键节点(如循环开始、条件判断前后、函数调用后)插入 echo 语句,输出关键变量的值和执行流程标记。对于生产环境的脚本,应将这些信息重定向到日志文件,便于事后分析。

代码示例

我们将使用一个有 bug 的脚本来演示各种调试技巧。

1. 问题脚本 calculate_avg.sh

#!/bin/bash
# 计算一系列数字的平均值
# 故意引入两个错误:1. 变量名拼写错误 2. 除零风险

numbers=(10 20 30 40)
total=0
count=0

# 使用 `for` 循环时,`count` 变量名拼写错误,写成了 `coutn`
for num in ${numbers[@]}; do
    total=$((total + num))
    coutn=$((count + 1))  # 错误1:变量名错误
done

# 以下模拟一个可能为空的列表
empty_numbers=()
sum2=0
for i in ${empty_numbers[@]}; do
    sum2=$((sum2 + i))
done

# 计算平均值,如果列表为空,count 仍为 0,会导致除零错误
average=$((total / count))  # 错误2:除零风险
echo "The average is: $average"

2. 使用 set -x 进行快速诊断 在脚本开头添加 set -x,或直接以 bash -x ./calculate_avg.sh 运行。

$ bash -x ./calculate_avg.sh
+ numbers=(10 20 30 40)
+ total=0
+ count=0
+ for num in 10 20 30 40
+ total=10
+ coutn=1            # 注意这里,变量 coutn 被赋值,但 count 没变!
+ for num in 10 20 30 40
+ total=30
+ coutn=1            # 问题显现:count 始终为 0,coutn 从 1 覆盖到 4 但没用
...
+ empty_numbers=()
+ sum2=0
+ average=0          # total=100, count=0, 除零!得到的结果 0 是错的
./calculate_avg.sh: 行 18: 100/0: 语法错误: 需要被操作数 (错误符号是 “0/0”) # 最终报错

-x 的输出清晰地展示了 count 变量从未被更新,以及最终的除零错误。

3. 使用 bashdb 进行交互式调试

# 确保已安装 bashdb (如:sudo apt install bashdb)
$ bashdb ./calculate_avg.sh
bashdb (release 1.0-1.0): bash --debugger
Copyright 2002, 2003, 2004, 2006, 2007, 2009, 2011 Rocky Bernstein
Type ‘h’ for help.
(/path/to/calculate_avg.sh:3): 
3:      numbers=(10 20 30 40)
bashdb<0> n # 单步执行
4:      total=0
bashdb<0> n
5:      count=0
bashdb<0> p count # 打印变量 count 的值
0
bashdb<0> b 7 # 在 for 循环体内设置断点
Breakpoint 1 set at line 7.
bashdb<0> c # 继续运行到断点
Breakpoint 1 hit at line 7.
7:              total=$((total + num))
bashdb<0> p total num # 一次性打印多个变量
total=0
num=10
bashdb<0> s # 单步进入(虽然这里没有函数调用,但可以观察下一行)
8:                  coutn=$((count + 1))
bashdb<0> p count # 在计算 coutn 前检查 count
0
bashdb<0> n # 执行下一行
7:              total=$((total + num))
bashdb<0> p count coutn # 发现问题:count 还是 0,而 coutn 是 1
count=0
coutn=1
bashdb<0> q # 退出调试器

通过 bashdb,我们交互式地确认了 count 变量的错误。

实践练习

练习 1:基础调试 给定脚本 loop_sum.sh,它试图计算 1 到 5 的和,但结果错误。

#!/bin/bash
sum=0
i=1
while [ $i -le 5 ]; do
    sum=$sum + $i  # 这里缺少命令替换 `$(( ))`
    i=$((i+1))
done
echo “Sum from 1 to 5 is: $sum

要求:仅使用 set -xbash -x 命令定位错误,并在不修改脚本的前提下,观察错误输出。指出问题所在行。

练习 2:使用调试器 使用 bashdb 调试以下脚本 check_number.sh,找出变量 result 的值何时被错误地设置。

#!/bin/bash
read -p “Enter a number: ” num
if [ $num -gt 10 ]; then
    result=“big”
elif [ $num -gt 5 ]; then
    result=“medium”
else
    result=“small”
fi
echo “The number is $result

要求

  1. bashdb 中,输入数字 7
  2. echo 语句前设置断点。
  3. 单步执行 if-elif-else 结构,每次使用 p result 查看 result 的值。
  4. 描述执行流程。

练习 3:综合调试 编写一个名为 safe_divide.sh 的脚本,接受两个参数 $1$2,计算它们的商。脚本需要:

  1. 检查是否提供了两个参数。
  2. 检查第二个参数是否为 0。
  3. 使用 set -eu 确保安全运行。
  4. 在关键步骤添加调试输出(如 echo “DEBUG: Checking arguments…”)。 要求:先正常运行测试,然后模拟错误场景(不提供参数、除零),观察 set -eu 和调试输出的效果。

常见错误

  1. 忽视 set -x 的输出:输出可能很长,初学者容易被“信息淹没”。建议关注第一次出现错误提示前的几行输出,它们往往包含线索。
  2. set -e 的误用:它会导致脚本在第一个错误后退出,有时这并非所愿(例如,一个命令失败不影响整体)。调试时,可以临时用 set +e 局部关闭,或使用 command || true 来避免退出。
  3. 忘记行号set -x 输出不直接显示行号。结合 vimgrep -n 查看源代码行号,与调试输出对照。
  4. bashdb 中忘记设置断点:直接运行 c 可能会很快结束,无法在需要的地方停下来。调试前应先用 l 列出代码,规划好断点位置。
  5. 变量名拼写错误是“隐形杀手”set -u 是检测未定义变量的利器,应在开发阶段就养成开启的习惯。

小结

Shell 脚本调试是一项核心的运维技能。本课我们学习了三种递进的调试方法:

  • 快速透视:使用 bash -x script.shset -x,能直观看到每条命令的执行情况,是解决大多数问题的首选。
  • 交互式手术刀bashdb 提供了设置断点、单步执行、检查变量等强大功能,适用于复杂逻辑的深度调试。
  • 辅助检查:善用 echo 输出和 set -u 等安全选项,可以预防和发现许多低级错误。

记住,调试不是猜谜,而是有策略的侦察。从最简单的 set -x 开始,逐步升级到交互式调试器,并养成良好的编码习惯(如使用有意义的变量名、添加日志),你的脚本将变得更健壮、更易维护。

练习编辑器

rust
Loading...

继续学习

完成本课后,建议继续学习下一课「实用脚本编写实战」 以巩固所学知识。