调试任何程序时,我们最常做的事情一般有下面几个
- 查看上下文变量
- 查看堆栈
- 单步执行
在Shell中,我们有多种方式来获取环境信息,如下所示
export -p或env: 查看当前export的变量
declare: 查看当前Bash上下文中的各种值
-a: 查看数组-A: 查看关联数组-f:查看函数和定义-F:只查看函数名称
不过假如当我们想知道,某个函数的定义在哪个文件里,该如何追踪呢?这里需要用到shopt命令,它可以改变Shell的某些额外行为,使用方法如下
synopsis:
不带参数: 打印所有选项的名称和对应值
shopt 选项名: 只打印这个选项的值
-s 选项名: 启用该选项
-u 选项名: 禁用该选项
-p: 打印所有支持的选项名
-o: 只打印可以用于set -o 选项名命令的选项名
shopt的选项中有一个专门用于Shell调试的选项名,叫做extdebug,默认值为关闭,我们先来开启它。
$ shopt extdebug
extdebug off
$ shopt -s extdebug
$ shopt extdebug
extdebug on
$ shopt | grep extdebug
extdebug on
开启它之后,我们就可以找到任意函数的定义位置。在我的Bash中有一个Shell启动后自带的quote函数,可以查看它的定义
$ declare -f quote
quote ()
{
local quoted=${1//'/'\''};
printf "'%s'" "$quoted"
}
进一步查看它定义位置的行号和文件名为
$ declare -F quote
quote 132 /usr/share/bash-completion/bash_completion
看,很容易吧!
调试的第二个方面,是打印出当前的函数调用栈,下面这个函数可以做到这一点
function show_callstack()
{
declare -i level=${#FUNCNAME[*]}
for (( i = 1; i < level; i++))
do
echo ${FUNCNAME[$i]} ${BASH_SOURCE[i + 1]}:${BASH_LINENO[$i]}
done
}
FUNCNAME变量中保存了当前调用栈中每一层函数的名称,BASH_SOURCE中保存了文件名(下标比FUNCNAME多1),BASH_LINE_NO保存了行号。
参考程序示例
1 #!/bin/bash
2
3 function show_callstack()
4 {
5 declare -i level=${#FUNCNAME[*]}
6 for (( i = 1; i < level; i++))
7 do
8 echo ${FUNCNAME[i]} at ${BASH_SOURCE[i + 1]}:${BASH_LINENO[i]}
9 done
10 }
11
12 function C()
13 {
14 error_here
15 show_callstack
16 }
17
18 function B()
19 {
20 C
21 }
22
23 function A()
24 {
25 B
26 }
27
28 A
./show_callstack.sh:行14: error_here: 未找到命令
C at ./show_callstack.sh:20
B at ./show_callstack.sh:25
A at ./show_callstack.sh:28
main at :0
更进一步,我们可以使用shell的trap命令,在脚本运行发生错误(非0返回值)时自动调用show_callstack打印堆栈,为此我们需要在上面脚本开始位置加入下面两行
set -E
trap show_callstack ERR
第一句启用errtrace,在函数调用时继承ERR trap,第二句让发生ERR时执行show_callstack。
脚本执行结果为
./show_callstack.sh:行18: error_here: 未找到命令
C at ./show_callstack.sh:23
B at ./show_callstack.sh:28
A at ./show_callstack.sh:31
main at :0
B at ./show_callstack.sh:28
A at ./show_callstack.sh:31
main at :0
A at ./show_callstack.sh:31
main at :0
main at :0
多次重复打印是因为C,B,A每一处调用位置,都触发了show_callstack(如何优化,留给读者思考)。
PS: 下面的内容是Bash手册中trap命令的摘要
trap [-lp] [[arg] sigspec ...]
The command arg is to be read and executed when the shell receives signal(s) sigspec. If arg is absent (and there is a single sigspec) or -, each specified signal is reset to its
original disposition (the value it had upon entrance to the shell). If arg is the null string the signal specified by each sigspec is ignored by the shell and by the commands it
invokes. If arg is not present and -p has been supplied, then the trap commands associated with each sigspec are displayed. If no arguments are supplied or if only -p is given,
trap prints the list of commands associated with each signal. The -l option causes the shell to print a list of signal names and their corresponding numbers. Each sigspec is ei-
ther a signal name defined in <signal.h>, or a signal number. Signal names are case insensitive and the SIG prefix is optional.
If a sigspec is ERR, the command arg is executed whenever a pipeline (which may consist of a single simple command), a list, or a compound command returns a non-zero exit status,
subject to the following conditions. The ERR trap is not executed if the failed command is part of the command list immediately following a while or until keyword, part of the test
in an if statement, part of a command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command's return
value is being inverted using !. These are the same conditions obeyed by the errexit (-e) option.

浙公网安备 33010602011771号