当我第一次要调试一个Bash脚本时,我完全是迷茫的。我只希望我有我常用的调试工具箱来调试这个环境。幸运的是,它支持其他语言的大部分方法。这篇文章是关于如何对shell脚本进行整理、跟踪和调试。
用ShellCheck做代码检查
在很多情况下,一个看似完美的脚本由于少了一个分号或空格而无法工作。除此以外,Bash中的许多构造都有奇怪的边缘情况,有些命令在所有环境下可能无法正常工作。
Linters会对代码进行静态分析,以确定其中的许多情况。从他们提供的几乎是即时的反馈中可以学到很多东西。
ShellCheck就是这样一个针对Bash的工具。
为了证明它,让我们来看看这个脚本:
#!/bin/bash var = 42 echo $var
执行它的结果是这样的错误:./awesome.sh: 第3行:var: command not found. 这就有点隐晦了。让我们来看看ShellCheck是怎么说的:
? shellcheck awesome.sh
In awesome.sh line 3:var = 42 ^-- SC1068: Don't put spaces around the = in assignments (or quote to make it literal).
For more information: https://www.shellcheck.net/wiki/SC1068 -- Don't put spaces around the = in ...
aha,这就很有帮助了! ShellCheck中有超过300多条的规则来捕捉类似这样的问题,它甚至支持自动修复部分案例。
ShellCheck你可以在本地安装,也可以使用官方的Docker镜像。
docker run --rm -it -v $(pwd):/mnt koalaman/shellcheck *.sh
打印调试
通常情况下,弄清楚一个脚本的控制流程并不明显,但你必须找出代码的哪一部分被执行了。一种方法是老式的print调试,在代码中插入echo语句。这种调试方式很直接,也很容易使用。一个可能的弊端是,echo到函数的标准输出,可能会改变程序的行为。
因为错误流的使用较少,通常情况下,使用它来调试是个好主意。
if [[ some_condition ]]; then echo "Branch 1 executed" >&2 ...else echo "Branch 2 executed" >&2 ...fi
一个更稳健的方法是使用logger写入系统日志中去。
跟踪(Tracing)
对于更复杂的脚本,更简单的替代方法是启用Tracing。当启用Tracing时,每个命令及其参数在执行之前都会被打印出来,这样就可以观察到程序的整个流程。
让我们看看它是如何运作的。对于本例,请考虑以下计算冰激凌价格的脚本。每一份的底价是100,但如果你至少订3份,奇数天可以打折。
#!/bin/bashfunction calculatePrice() { if [[ $numberOfPortions -lt 3 ]]; then echo "100" else day=$(date -d "$D" '+%d') if (( $day % 2 )); then # Discount on odd days echo "80" else echo "100" fi fi}
numberOfPortions=$1pricePerPortion=$(calculatePrice $numberOfPortions)totalPrice=$(( $numberOfPortions * $pricePerPortion ))
echo "Total $totalPrice"
该脚本实现了用部分数字作为参数,并打印出计算后总金额数字的功能。
? ./ice_cream_price.sh 4Total 320
阅读脚本以找出为什么会产生这样的结果并不总是很容易,让我们看看Tracing如何帮助更好地理解这个程序。要启用它,可以在脚本的开头添加set -x,或者简单地将-x作为参数传递给Bash。
? bash -x ./ice_cream_price.sh 4+ numberOfPortions=4++ calculatePrice 4++ [[ 4 -lt 3 ]]+++ date -d '' +%d++ day=11++ (( 11 % 2 ))++ echo 80+ pricePerPortion=80+ totalPrice=320+ echo 'Total 320'Total 320
可以在任何给定的行上启用和禁用Tracing,因此可以将调试输出减少到某些部分。
this_call_is_not_tracedset -x # enable tracingtricky_functionset +x # disable tracingthis_call_is_not_traced
还可以通过设置PS4变量来定制跟踪消息,这样可以获得更多的信息。下面的示例增强了Tracing输出,以包括文件的名称、函数和行号:
? export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'? bash -x ./ice_cream_price.sh 4+(./ice_cream_price.sh:19): numberOfPortions=4++(./ice_cream_price.sh:20): calculatePrice 4++(./ice_cream_price.sh:6): calculatePrice(): [[ 4 -lt 3 ]]+++(./ice_cream_price.sh:9): calculatePrice(): date -d '' +%d++(./ice_cream_price.sh:9): calculatePrice(): day=11++(./ice_cream_price.sh:10): calculatePrice(): (( 11 % 2 ))++(./ice_cream_price.sh:14): calculatePrice(): echo 80+(./ice_cream_price.sh:20): pricePerPortion=80+(./ice_cream_price.sh:21): totalPrice=320+(./ice_cream_price.sh:23): echo 'Total 320'Total 320
断点(Breakpoints)
有时,在任何给定的点停止程序执行来逐步执行命令并查看它们的行为是非常方便的。幸运的是,在某种程度上,这在巴斯是可能的。
与打印调试类似,可以向代码中添加额外的read命令来停止脚本执行,直到手动干预为止。
command_1
echo "Press enter to continue!" >&2read
command_2
如果您想在给定的步骤中检查脚本的效果,这可能会很方便。
还可以通过添加read作为调试陷阱来逐步执行整个脚本(或脚本的一部分)。最好与Tracing一起使用,以查看执行了哪些命令:
# Enable debuggingset -xtrap read debug
...
# Disable debuggingset -xtrap - debug
总结
调试Bash脚本不是一项容易的任务。这种语言比我使用过的其他语言有更多的粗糙边缘,而且通常这种工具只是一个文本编辑器。在这种情况下,了解使这一具有挑战性的任务更有效的可用工具是非常重要的。