在shell脚本中输出调试信息
通过在程序中加入调试语句把一些关键地方或出错的地方的相关信息显示出来是最常见的调试手段。
Shell程序员通常使用echo(ksh程序员常使用print)语句输出信息,但仅仅依赖echo语句的输出跟踪信息很麻烦,调试阶段在脚本中加入的大量的echo语句在产品交付时还得再费力一一删除。针对这个问题,本节主要介绍一些如何方便有效的输出调试信息的方法。
1. 使用trap命令
trap命令用于捕获指定的信号并执行预定义的命令。
其基本的语法是:
trap 'command' signal
其中signal是要捕获的信号,command是捕获到指定的信号之后,所要执行的命令。可以用kill –l命令看到系统中全部可用的信号名,捕获信号后所执行的命令可以是任何一条或多条合法的shell语句,也可以是一个函数名。
shell脚本在执行时,会产生三个所谓的“伪信号”,(之所以称之为“伪信号”是因为这三个信号是由shell产生的,而其它的信号是由操作系统产生的),通过使用trap命令捕获这三个“伪信号”并输出相关信息对调试非常有帮助。
表 1. shell伪信号
信号名 何时产生
EXIT 从一个函数中退出或整个脚本执行完毕
ERR 当一条命令返回非零状态时(代表命令执行不成功)
DEBUG 脚本中每一条命令执行之前
通过捕获EXIT信号,我们可以在shell脚本中止执行或从函数中退出时,输出某些想要跟踪的变量的值,并由此来判断脚本的执行状态以及出错原因,其使用方法是:
trap 'command' EXIT 或 trap 'command' 0
通过捕获ERR信号,我们可以方便的追踪执行不成功的命令或函数,并输出相关的调试信息,以下是一个捕获ERR信号的示例程序,其中的$LINENO是一个shell的内置变量,代表shell脚本的当前行号。
$ cat -n exp1.sh
1 ERRTRAP()
2 {
3 echo "[LINE:$1] Error: Command or function exited with status $?"
4 }
5 foo()
6 {
7 return 1;
8 }
9 trap 'ERRTRAP $LINENO' ERR
10 abc
11 foo
以下是一个通过捕获DEBUG信号来跟踪变量的示例程序:(对变量进行全程跟踪)
$ cat –n exp2.sh
1 #!/bin/bash
2 trap 'echo “before execute line:$LINENO, a=$a,b=$b,c=$c”' DEBUG
3 a=1
4 if [ "$a" -eq 1 ]
5 then
6 b=2
7 else
8 b=1
9 fi
10 c=3
11 echo "end"
2. 使用tee命令
在shell脚本中管道以及输入输出重定向使用得非常多,在管道的作用下,一些命令的执行结果直接成为了下一条命令的输入。如果我们发现由管道连接起来的一批命令的执行结果并非如预期的那样,就需要逐步检查各条命令的执行结果来判断问题出在哪儿,但因为使用了管道,这些中间结果并不会显示在屏幕上,给调试带来了困难,此时我们就可以借助于tee命令了。
tee命令会从标准输入读取数据,将其内容输出到标准输出设备,同时又可将内容保存成文件。例如有如下的脚本片段,其作用是获取本机的ip地址:
ipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'
| cut -d : -f3 | awk '{print $1}'`
#注意=号后面的整句是用反引号(数字1键的左边那个键)括起来的。
echo $ipaddr
将这段脚本再执行一遍,然后查看temp.txt文件的内容:
我们可以发现中间结果的第二列(列之间以:号分隔)才包含了IP地址,而在上面的脚本中使用cut命令截取了第三列,故我们只需将脚本中的cut -d : -f3改为cut -d : -f2即可得到正确的结果。
具体到上述的script例子,我们也许并不需要tee命令的帮助,比如我们可以分段执行由管道连接起来的各条命令并查看各命令的输出结果来诊断错误,
但在一些复杂的shell脚本中,这些由管道连接起来的命令可能又依赖于脚本中定义的一些其它变量,这时我们想要在提示符下来分段运行各条命令就会非常麻烦了,简单地在管道之间插入一条tee命令来查看中间结果会更方便一些。
3. 使用"调试钩子"
在C语言程序中,我们经常使用DEBUG宏来控制是否要输出调试信息,在shell脚本中我们同样可以使用这样的机制,如下列代码所示:
if [ “$DEBUG” = “true” ]; then
echo “debugging” #此处可以输出调试信息
$@
fi
shell版本的调试钩子。
DEBUG(){ [ "$DEBUG" = 0 ] && { echo "${BASH_LINENO}:$@ _"; $@; }; }
通过定义一个DEBUG函数可以使植入调试钩子的过程更简洁方便,如下面代码所示:
$ cat –n exp3.sh
1 DEBUG()
2 {
3 if [ "$DEBUG" = "true" ]; then
4 $@
5 fi
6 }
7 a=1
8 DEBUG echo "a=$a"
9 if [ "$a" -eq 1 ]
10 then
11 b=2
12 else
13 b=1
14 fi
15 DEBUG echo "b=$b"
16 c=3
17 DEBUG echo "c=$c"
DEBUG=true
4. 使用SHELL的执行选项
上一节所述的调试手段是通过修改shell脚本的源代码,令其输出相关的调试信息来定位错误的,那有没有不修改源代码来调试shell脚本的方法呢?答案就是使用shell的执行选项,本节将介绍一些常用选项的用法:
-n 只读取shell脚本,但不实际执行
-x 进入跟踪方式,显示所执行的每一条命令
-c "string" 从strings中读取命令
“-n”可用于测试shell脚本是否存在语法错误,但不会实际执行命令。在shell脚本编写完成之后,实际执行之前,首先使用“-n”选项来测试脚本是否存在语法错误是一个很好的习惯。
因为某些shell脚本在执行时会对系统环境产生影响,比如生成或移动文件等,如果在实际执行才发现语法错误,您不得不手工做一些系统环境的恢复工作才能继续测试这个脚本。
“-c”选项使shell解释器从一个字符串中而不是从一个文件中读取并执行shell命令。当需要临时测试一小段脚本的执行结果时,可以使用这个选项,如下所示:
sh -c 'a=1;b=2;let c=$a+$b;echo "c=$c"'
"-x"选项可用来跟踪脚本的执行,是调试shell脚本的强有力工具。“-x”选项使shell在执行脚本的过程中把它实际执行的每一个命令行显示出来,并且在行首显示一个"+"号。
"+"号后面显示的是经过了变量替换之后的命令行的内容,有助于分析实际执行的是什么命令。 “-x”选项使用起来简单方便,可以轻松对付大多数的shell调试任务,应把其当作首选的调试手段。
如果把本文前面所述的trap ‘command’ DEBUG机制与“-x”选项结合起来,我们就可以既输出实际执行的每一条命令,又逐行跟踪相关变量的值,对调试相当有帮助。
仍以前面所述的exp2.sh为例,现在加上“-x”选项来执行它:
$ sh –x exp2.sh
+ trap 'echo "before execute line:$LINENO, a=$a,b=$b,c=$c"' DEBUG
++ echo 'before execute line:3, a=,b=,c='
before execute line:3, a=,b=,c=
+ a=1
++ echo 'before execute line:4, a=1,b=,c='
before execute line:4, a=1,b=,c=
+ '[' 1 -eq 1 ']'
++ echo 'before execute line:6, a=1,b=,c='
before execute line:6, a=1,b=,c=
+ b=2
++ echo 'before execute line:10, a=1,b=2,c='
before execute line:10, a=1,b=2,c=
+ c=3
++ echo 'before execute line:11, a=1,b=2,c=3'
before execute line:11, a=1,b=2,c=3
+ echo end
end
在上面的结果中,前面有“+”号的行是shell脚本实际执行的命令,前面有“++”号的行是执行trap机制中指定的命令,其它的行则是输出信息。
shell的执行选项除了可以在启动shell时指定外,亦可在脚本中用set命令来指定。 "set -参数"表示启用某选项,"set +参数"表示关闭某选项。有时候我们并不需要在启动时用"-x"选项来跟踪所有的命令行,这时我们可以在脚本中使用set命令,如以下脚本片段所示:
set命令同样可以使用上一节中介绍的调试钩子—DEBUG函数来调用,这样可以避免脚本交付使用时删除这些调试语句的麻烦,如以下脚本片段所示:
DEBUG set -x #启动"-x"选项
要跟踪的程序段
DEBUG set +x #关闭"-x"选项
5. 对“-x”选项的增强
"-x"执行选项是目前最常用的跟踪和调试shell脚本的手段,但其输出的调试信息仅限于进行变量替换之后的每一条实际执行的命令以及行首的一个"+"号提示符,居然连行号这样的重要信息都没有,
对于复杂的shell脚本的调试来说,还是非常的不方便。幸运的是,我们可以巧妙地利用shell内置的一些环境变量来增强"-x"选项的输出信息,下面先介绍几个shell内置的环境变量:
$LINENO
代表shell脚本的当前行号,类似于C语言中的内置宏__LINE__
$FUNCNAME
函数的名字,类似于C语言中的内置宏__func__,但宏__func__只能代表当前所在的函数名,而$FUNCNAME的功能更强大,它是一个数组变量,其中包含了整个调用链上所有的函数的名字,故变量${FUNCNAME[0]}代表shell脚本当前正在执行的函数的名字,而变量${FUNCNAME[1]}则代表调用函数${FUNCNAME[0]}的函数的名字,余者可以依此类推。
$PS4
主提示符变量$PS1和第二级提示符变量$PS2比较常见,但很少有人注意到第四级提示符变量$PS4的作用。我们知道使用“-x”执行选项将会显示shell脚本中每一条实际执行过的命令,而$PS4的值将被显示在“-x”选项输出的每一条命令的前面。在Bash Shell中,缺省的$PS4的值是"+"号。(现在知道为什么使用"-x"选项时,输出的命令前面有一个"+"号了吧?)。
利用$PS4这一特性,通过使用一些内置变量来重定义$PS4的值,我们就可以增强"-x"选项的输出信息。例如先执行export PS4='+{$LINENO:${FUNCNAME[0]}} ', 然后再使用“-x”选项来执行脚本,就能在每一条实际执行的命令前面显示其行号以及所属的函数名。
以下是一个存在bug的shell脚本的示例,本文将用此脚本来示范如何用“-n”以及增强的“-x”执行选项来调试shell脚本。这个脚本中定义了一个函数isRoot(),用于判断当前用户是不是root用户,如果不是,则中止脚本的执行
$ cat –n exp4.sh
1 #!/bin/bash
2 isRoot()
3 {
4 if [ "$UID" -ne 0 ]
5 return 1
6 else
7 return 0
8 fi
9 }
10 isRoot
11 if ["$?" -ne 0 ]
12 then
13 echo "Must be root to run this script"
14 exit 1
15 else
16 echo "welcome root user"
17 #do something
18 fi
例如我们可以试试定制$PS4的值,并使用“-x”选项来跟踪:
$ export PS4='+{$LINENO:${FUNCNAME[0]}} '
$ sh –x exp4.sh
+{10:} isRoot
+{4:isRoot} '[' 503 -ne 0 ']'
+{5:isRoot} return 1
+{11:} '[1' -ne 0 ']'
exp4.sh: line 11: [1: command not found
+{16:} echo 'welcome root user'
welcome root user
shell中还有其它一些对调试有帮助的内置变量,比如在Bash Shell中还有BASH_SOURCE, BASH_SUBSHELL等一批对调试有帮助的内置变量,您可以通过man sh或man bash来查看,然后根据您的调试目的,使用这些内置变量来定制$PS4,从而达到增强“-x”选项的输出信息的目的。
五. 总结
现在让我们来总结一下调试shell脚本的过程:
首先使用“-n”选项检查语法错误,然后使用“-x”选项跟踪脚本的执行,
使用“-x”选项之前,别忘了先定制PS4变量的值来增强“-x”选项的输出信息,至少应该令其输出行号信息
(先执行export PS4='+[$LINENO]',更一劳永逸的办法是将这条语句加到您用户主目录的.bash_profile文件中去),
这将使你的调试之旅更轻松。也可以利用trap,调试钩子等手段输出关键调试信息,快速缩小排查错误的范围,并在脚本中使用“set -x”及“set +x”对某些代码块进行重点跟踪。
这样多种手段齐下,相信您已经可以比较轻松地抓出您的shell脚本中的臭虫了。
如果您的脚本足够复杂,还需要更强的调试能力,可以使用shell调试器bashdb,这是一个类似于GDB的调试工具,可以完成对shell脚本的断点设置,单步执行,变量观察等许多功能,
使用bashdb对阅读和理解复杂的shell脚本也会大有裨益。关于bashdb的安装和使用,不属于本文范围,您可参阅http://bashdb.sourceforge.net/上的文档并下载试用。
六. 一些读写脚本的技巧。
vim + ctags 阅读脚本。
ctags 默认操作
ctags -R 生成某一目录的tag文件。
ctags -f 生成的tag文件名。
例如 ctags -f test_tags ./test.sh
ctags -f test1_tags -R ./test1/
vim 中添加 tag 标记文件。
vim 中: set tags+=/tmp/test_tags,/tmp/test1_tags
vim 中: set path += /dev/shm/,/tmp/test1/*
然后再vim中就可以使用
跳转到一个主题: 将光标置于标签 (例如 |bars|) 上然后输入 CTRL-]。
跳回: 键入 CTRL-T 或 CTRL-O
七.预定义变量
$#位置参数的数量 $*所有位置参数的内容 $?命令执行后返回的状态 一般为0
$$当前进程号 $!后台运行最后一进程号 $0当前执行的进程名
位置参数是一种在调用shell程序的命令行中按照各自的位置决定的变量,是在程序名之后输入的参数。位置参数之间用空格分隔,shell取第一个位置参数替换程序文件中的$1,第二个替换$2,依次类推。$0是一个特殊的变量,它的内容是当前这个shell程序的文件名即脚本名。
参数置换的变量
变量=${参数-word}: 如果设置了参数,则用参数的值置换变量的值,否则用word置换。
变量=${参数=word}: 如果设置了参数,则用参数的值置换变量的值,否则把变量设置成word然后再用word替换参数的值
变量=${参数?word}: 如果设置了参数,则用参数的值置换变量的值,否则就显示word并从shell中退出,如果省略了word,则显示标准信息。
变量=${参数+word}: 如果设置了参数,则用word置换变量,否则不进行置换。
本地变量
ABC="abc"
echo ${ABC}
清除变量 unset variable_name
显示所有shell变量 set set -a指明所有变量直接被导出
设置只读变量 readonly variable_name
查看所有环境变量 env
八.嵌入shell变量
CDPATH cd进入CDPATH指定的路径
EXINIT 保存使用vi初始化选项 EXINIT='set nu tab=4'; export EXINIT
IFS 用作shell指定的缺省域分隔符 export IFS=:
PS1 基本提示符包含shell提示符
PS2 附属提示符 缺省>
TERM 保存终端类型
EDITOR
PAGER
九.常用shell命名:
tty 报告所连接的设备或终端 -s确认脚本的标准输入 0终端 1非终端
uname option -a 显示所有信息 -s系统名 -v只显示操作系统版本或发布日期
wait process_id 等待process ID进程或所有进程结束
bashname 从路径中分离出文件名
cat option file 文本文件显示命令 -v显示控制字符
touch option file 以当前时间创建文件的时间戳 -tMMDDhhmm
head -num file 显示文件的前n行
tail -num file 显示文件的后n行
more options file 分屏显示文件内容 -c不滚屏 -d在分页处显示提示 -n每屏显示n行
nl option file 在文件中列行号 -I行号每次增加n
string file 查看二进制文件中包含的文本
wc option file 统计文件中字符数、单词数和行数 -c字符数 -l行数 -w单词数
du -sh file/dir 显示大小 -a 显示每个文件的大小 -s只显示总计
fuser option file -k杀死所有访问该文件的进程 -u/m显示访问该文件系统的所有进程
ldd a.out 显示可执行文件所需的动态链接库
readelf 识别目标系统的程序所依赖的动态链接库
file a.out 显示文件格式
compress option files 压缩文件 .Z -v显示压缩结果
uncompass files 解压缩文件 不必输入.Z
diff option file1 file1显示两个文件中不一致的行 -c按照标准格式输出 -I 忽略大小写
logname 显示登陆名
script option file 用script命令纪录当前会话 -a将输出附加到文件末尾
shutdown now / -g60 -I6 -y 60秒后关机 然后重起
sleep number 系统等待指定的钞数
whereis cmd 系统命令的路径
who options -a -r -s
whoami
hostname
hostid
adduser
passwd
dmesg
uname --help -a(all infomation) -s(kernel-name) -n(network hostname) -r(kernel-release) -v(kernel-version) -m(machine) -p(processor) -i(hardware-platform) -o(operationg-system)