第49课:Shell 脚本调试技巧
所属模块: Shell 脚本进阶 难度: Intermediate 标签: debug, bashdb 上一课: trap 信号处理 下一课: 实用脚本编写实战
学习目标
学完本课,你将能够:
- 熟练使用
set命令系列(如-x,-v)对脚本进行快速跟踪和输出调试。 - 掌握利用
bashdb这一交互式调试器,对复杂脚本进行设置断点、单步执行和变量检查。 - 学会结合
echo和日志输出进行关键位置的状态监控。 - 能够高效地定位并修复脚本中的逻辑错误和运行时错误。
核心概念
调试脚本就像是侦探破案,你需要找到“犯罪现场”(出错的位置)和“犯罪动机”(错误的原因)。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 -x 或 bash -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”
要求:
- 在
bashdb中,输入数字7。 - 在
echo语句前设置断点。 - 单步执行
if-elif-else结构,每次使用p result查看result的值。 - 描述执行流程。
练习 3:综合调试
编写一个名为 safe_divide.sh 的脚本,接受两个参数 $1 和 $2,计算它们的商。脚本需要:
- 检查是否提供了两个参数。
- 检查第二个参数是否为 0。
- 使用
set -eu确保安全运行。 - 在关键步骤添加调试输出(如
echo “DEBUG: Checking arguments…”)。 要求:先正常运行测试,然后模拟错误场景(不提供参数、除零),观察set -eu和调试输出的效果。
常见错误
- 忽视
set -x的输出:输出可能很长,初学者容易被“信息淹没”。建议关注第一次出现错误提示前的几行输出,它们往往包含线索。 set -e的误用:它会导致脚本在第一个错误后退出,有时这并非所愿(例如,一个命令失败不影响整体)。调试时,可以临时用set +e局部关闭,或使用command || true来避免退出。- 忘记行号:
set -x输出不直接显示行号。结合vim或grep -n查看源代码行号,与调试输出对照。 bashdb中忘记设置断点:直接运行c可能会很快结束,无法在需要的地方停下来。调试前应先用l列出代码,规划好断点位置。- 变量名拼写错误是“隐形杀手”:
set -u是检测未定义变量的利器,应在开发阶段就养成开启的习惯。
小结
Shell 脚本调试是一项核心的运维技能。本课我们学习了三种递进的调试方法:
- 快速透视:使用
bash -x script.sh或set -x,能直观看到每条命令的执行情况,是解决大多数问题的首选。 - 交互式手术刀:
bashdb提供了设置断点、单步执行、检查变量等强大功能,适用于复杂逻辑的深度调试。 - 辅助检查:善用
echo输出和set -u等安全选项,可以预防和发现许多低级错误。
记住,调试不是猜谜,而是有策略的侦察。从最简单的 set -x 开始,逐步升级到交互式调试器,并养成良好的编码习惯(如使用有意义的变量名、添加日志),你的脚本将变得更健壮、更易维护。