本文全面系統(tǒng)地介紹了shell腳本調(diào)試技術(shù),包括使用echo, tee, trap等命令輸出關(guān)鍵信息但惶,跟蹤變量的值枷遂,在腳本中植入調(diào)試鉤子,使用“-n”選項(xiàng)進(jìn)行shell腳本的語法檢查竞川, 使用“-x”選項(xiàng)實(shí)現(xiàn)shell腳本逐條語句的跟蹤店溢,巧妙地利用shell的內(nèi)置變量增強(qiáng)“-x”選項(xiàng)的輸出信息等叁熔。
曹 羽中(caoyuz@cn.ibm.com), 軟件工程師, IBM中國開發(fā)中心
一. 前言
shell編程在unix/linux世界中使用得非常廣泛,熟練掌握shell編程也是成為一名優(yōu)秀的unix/linux開發(fā)者和系統(tǒng)管理員的必經(jīng)之路床牧。腳本調(diào)試的主要工作就是發(fā)現(xiàn)引發(fā)腳本錯誤的原因以及在腳本源代碼中定位發(fā)生錯誤的行荣回,常用的手段包括分析輸出的錯誤信息,通過在腳本中加入調(diào)試語句戈咳,輸出調(diào)試信息來輔助診斷錯誤心软,利用調(diào)試工具等。但與其它高級語言相比著蛙,shell解釋器缺乏相應(yīng)的調(diào)試機(jī)制和調(diào)試工具的支持删铃,其輸出的錯誤信息又往往很不明確,初學(xué)者在調(diào)試腳本時踏堡,除了知道用echo語句輸出一些信息外泳姐,別無它法沮焕,而僅僅依賴于大量的加入echo語句來診斷錯誤肋联,確實(shí)令人不勝其繁罗晕,故常見初學(xué)者抱怨shell腳本太難調(diào)試了谎势。本文將系統(tǒng)地介紹一些重要的shell腳本調(diào)試技術(shù)境输,希望能對shell的初學(xué)者有所裨益燥滑。
本文的目標(biāo)讀者是unix/linux環(huán)境下的開發(fā)人員改鲫,測試人員和系統(tǒng)管理員窗市,要求讀者具有基本的shell編程知識肮街。本文所使用范例在Bash3.1+Redhat Enterprise Server 4.0下測試通過风题,但所述調(diào)試技巧應(yīng)也同樣適用于其它shell。
二. 在shell腳本中輸出調(diào)試信息
通過在程序中加入調(diào)試語句把一些關(guān)鍵地方或出錯的地方的相關(guān)信息顯示出來是最常見的調(diào)試手段嫉父。Shell程序員通常使用echo(ksh程序員常使用print)語句輸出信息沛硅,但僅僅依賴echo語句的輸出跟蹤信息很麻煩,調(diào)試階段在腳本中加入的大量的echo語句在產(chǎn)品交付時還得再費(fèi)力一一刪除绕辖。針對這個問題摇肌,本節(jié)主要介紹一些如何方便有效的輸出調(diào)試信息的方法。
1. 使用trap命令
trap命令用于捕獲指定的信號并執(zhí)行預(yù)定義的命令仪际。
其基本的語法是:
trap 'command' signal
其中signal是要捕獲的信號围小,command是捕獲到指定的信號之后,所要執(zhí)行的命令树碱】鲜剩可以用kill –l命令看到系統(tǒng)中全部可用的信號名,捕獲信號后所執(zhí)行的命令可以是任何一條或多條合法的shell語句成榜,也可以是一個函數(shù)名框舔。
shell腳本在執(zhí)行時,會產(chǎn)生三個所謂的“偽信號”,(之所以稱之為“偽信號”是因?yàn)檫@三個信號是由shell產(chǎn)生的刘绣,而其它的信號是由操作系統(tǒng)產(chǎn)生的)钳垮,通過使用trap命令捕獲這三個“偽信號”并輸出相關(guān)信息對調(diào)試非常有幫助。
表 1. shell偽信號
信號名何時產(chǎn)生
EXIT從一個函數(shù)中退出或整個腳本執(zhí)行完畢
ERR當(dāng)一條命令返回非零狀態(tài)時(代表命令執(zhí)行不成功)
DEBUG腳本中每一條命令執(zhí)行之前
通過捕獲EXIT信號,我們可以在shell腳本中止執(zhí)行或從函數(shù)中退出時额港,輸出某些想要跟蹤的變量的值,并由此來判斷腳本的執(zhí)行狀態(tài)以及出錯原因,其使用方法是:
trap 'command' EXIT 或 trap 'command' 0
通過捕獲ERR信號,我們可以方便的追蹤執(zhí)行不成功的命令或函數(shù)歧焦,并輸出相關(guān)的調(diào)試信息移斩,以下是一個捕獲ERR信號的示例程序,其中的$LINENO是一個shell的內(nèi)置變量绢馍,代表shell腳本的當(dāng)前行號向瓷。
$ 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
其輸出結(jié)果如下:
$ sh exp1.sh
exp1.sh: line 10: abc: command not found
[LINE:10] Error: Command or function exited with status 127
[LINE:11] Error: Command or function exited with status 1
在調(diào)試過程中,為了跟蹤某些變量的值舰涌,我們常常需要在shell腳本的許多地方插入相同的echo語句來打印相關(guān)變量的值猖任,這種做法顯得煩瑣而笨拙。而通過捕獲DEBUG信號瓷耙,我們只需要一條trap語句就可以完成對相關(guān)變量的全程跟蹤朱躺。
以下是一個通過捕獲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"
其輸出結(jié)果如下:
$ sh exp2.sh
before execute line:3, a=,b=,c=
before execute line:4, a=1,b=,c=
before execute line:6, a=1,b=,c=
before execute line:10, a=1,b=2,c=
before execute line:11, a=1,b=2,c=3
end
從運(yùn)行結(jié)果中可以清晰的看到每執(zhí)行一條命令之后,相關(guān)變量的值的變化搁痛。同時长搀,從運(yùn)行結(jié)果中打印出來的行號來分析,可以看到整個腳本的執(zhí)行軌跡鸡典,能夠判斷出哪些條件分支執(zhí)行了源请,哪些條件分支沒有執(zhí)行。
2. 使用tee命令
在shell腳本中管道以及輸入輸出重定向使用得非常多彻况,在管道的作用下谁尸,一些命令的執(zhí)行結(jié)果直接成為了下一條命令的輸入。如果我們發(fā)現(xiàn)由管道連接起來的一批命令的執(zhí)行結(jié)果并非如預(yù)期的那樣纽甘,就需要逐步檢查各條命令的執(zhí)行結(jié)果來判斷問題出在哪兒良蛮,但因?yàn)槭褂昧斯艿溃@些中間結(jié)果并不會顯示在屏幕上悍赢,給調(diào)試帶來了困難背镇,此時我們就可以借助于tee命令了。
tee命令會從標(biāo)準(zhǔn)輸入讀取數(shù)據(jù)泽裳,將其內(nèi)容輸出到標(biāo)準(zhǔn)輸出設(shè)備,同時又可將內(nèi)容保存成文件瞒斩。例如有如下的腳本片段,其作用是獲取本機(jī)的ip地址:
ipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'
| cut -d : -f3 | awk '{print $1}'`
#注意=號后面的整句是用反引號(數(shù)字1鍵的左邊那個鍵)括起來的涮总。
echo $ipaddr
運(yùn)行這個腳本胸囱,實(shí)際輸出的卻不是本機(jī)的ip地址,而是廣播地址,這時我們可以借助tee命令瀑梗,輸出某些中間結(jié)果烹笔,將上述腳本片段修改為:
ipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'
| tee temp.txt | cut -d : -f3 | awk '{print $1}'`
echo $ipaddr
之后裳扯,將這段腳本再執(zhí)行一遍,然后查看temp.txt文件的內(nèi)容:
$ cat temp.txt
inet addr:192.168.0.1? Bcast:192.168.0.255? Mask:255.255.255.0
我們可以發(fā)現(xiàn)中間結(jié)果的第二列(列之間以:號分隔)才包含了IP地址谤职,而在上面的腳本中使用cut命令截取了第三列饰豺,故我們只需將腳本中的cut -d : -f3改為cut -d : -f2即可得到正確的結(jié)果。
具體到上述的script例子允蜈,我們也許并不需要tee命令的幫助冤吨,比如我們可以分段執(zhí)行由管道連接起來的各條命令并查看各命令的輸出結(jié)果來診斷錯誤,但在一些復(fù)雜的shell腳本中饶套,這些由管道連接起來的命令可能又依賴于腳本中定義的一些其它變量漩蟆,這時我們想要在提示符下來分段運(yùn)行各條命令就會非常麻煩了,簡單地在管道之間插入一條tee命令來查看中間結(jié)果會更方便一些妓蛮。
3. 使用"調(diào)試鉤子"
在C語言程序中怠李,我們經(jīng)常使用DEBUG宏來控制是否要輸出調(diào)試信息,在shell腳本中我們同樣可以使用這樣的機(jī)制蛤克,如下列代碼所示:
if [ “$DEBUG” = “true” ]; then
echo “debugging”? #此處可以輸出調(diào)試信息
fi
這樣的代碼塊通常稱之為“調(diào)試鉤子”或“調(diào)試塊”捺癞。在調(diào)試鉤子內(nèi)部可以輸出任何您想輸出的調(diào)試信息,使用調(diào)試鉤子的好處是它是可以通過DEBUG變量來控制的构挤,在腳本的開發(fā)調(diào)試階段翘簇,可以先執(zhí)行export DEBUG=true命令打開調(diào)試鉤子,使其輸出調(diào)試信息儿倒,而在把腳本交付使用時版保,也無需再費(fèi)事把腳本中的調(diào)試語句一一刪除。
如果在每一處需要輸出調(diào)試信息的地方均使用if語句來判斷DEBUG變量的值夫否,還是顯得比較繁瑣彻犁,通過定義一個DEBUG函數(shù)可以使植入調(diào)試鉤子的過程更簡潔方便,如下面代碼所示:
$ 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函數(shù)中凰慈,會執(zhí)行任何傳給它的命令汞幢,并且這個執(zhí)行過程是可以通過DEBUG變量的值來控制的,我們可以把所有跟調(diào)試有關(guān)的命令都作為DEBUG函數(shù)的參數(shù)來調(diào)用微谓,非常的方便森篷。
三. 使用shell的執(zhí)行選項(xiàng)
上一節(jié)所述的調(diào)試手段是通過修改shell腳本的源代碼,令其輸出相關(guān)的調(diào)試信息來定位錯誤的豺型,那有沒有不修改源代碼來調(diào)試shell腳本的方法呢仲智?答案就是使用shell的執(zhí)行選項(xiàng),本節(jié)將介紹一些常用選項(xiàng)的用法:
-n 只讀取shell腳本姻氨,但不實(shí)際執(zhí)行
-x 進(jìn)入跟蹤方式钓辆,顯示所執(zhí)行的每一條命令
-c "string" 從strings中讀取命令
“-n”可用于測試shell腳本是否存在語法錯誤,但不會實(shí)際執(zhí)行命令。在shell腳本編寫完成之后前联,實(shí)際執(zhí)行之前功戚,首先使用“-n”選項(xiàng)來測試腳本是否存在語法錯誤是一個很好的習(xí)慣。因?yàn)槟承﹕hell腳本在執(zhí)行時會對系統(tǒng)環(huán)境產(chǎn)生影響似嗤,比如生成或移動文件等啸臀,如果在實(shí)際執(zhí)行才發(fā)現(xiàn)語法錯誤,您不得不手工做一些系統(tǒng)環(huán)境的恢復(fù)工作才能繼續(xù)測試這個腳本烁落。
“-c”選項(xiàng)使shell解釋器從一個字符串中而不是從一個文件中讀取并執(zhí)行shell命令乘粒。當(dāng)需要臨時測試一小段腳本的執(zhí)行結(jié)果時,可以使用這個選項(xiàng)顽馋,如下所示:
sh -c 'a=1;b=2;let c=$a+$b;echo "c=$c"'
"-x"選項(xiàng)可用來跟蹤腳本的執(zhí)行,是調(diào)試shell腳本的強(qiáng)有力工具幌羞〈缑眨“-x”選項(xiàng)使shell在執(zhí)行腳本的過程中把它實(shí)際執(zhí)行的每一個命令行顯示出來,并且在行首顯示一個"+"號属桦。 "+"號后面顯示的是經(jīng)過了變量替換之后的命令行的內(nèi)容熊痴,有助于分析實(shí)際執(zhí)行的是什么命令。 “-x”選項(xiàng)使用起來簡單方便聂宾,可以輕松對付大多數(shù)的shell調(diào)試任務(wù),應(yīng)把其當(dāng)作首選的調(diào)試手段果善。
如果把本文前面所述的trap ‘command’ DEBUG機(jī)制與“-x”選項(xiàng)結(jié)合起來,我們 就可以既輸出實(shí)際執(zhí)行的每一條命令系谐,又逐行跟蹤相關(guān)變量的值巾陕,對調(diào)試相當(dāng)有幫助。
仍以前面所述的exp2.sh為例纪他,現(xiàn)在加上“-x”選項(xiàng)來執(zhí)行它:
$ 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
在上面的結(jié)果中鄙煤,前面有“+”號的行是shell腳本實(shí)際執(zhí)行的命令,前面有“++”號的行是執(zhí)行trap機(jī)制中指定的命令茶袒,其它的行則是輸出信息梯刚。
shell的執(zhí)行選項(xiàng)除了可以在啟動shell時指定外,亦可在腳本中用set命令來指定薪寓。 "set -參數(shù)"表示啟用某選項(xiàng)亡资,"set +參數(shù)"表示關(guān)閉某選項(xiàng)。有時候我們并不需要在啟動時用"-x"選項(xiàng)來跟蹤所有的命令行向叉,這時我們可以在腳本中使用set命令锥腻,如以下腳本片段所示:
set -x #啟動"-x"選項(xiàng)
要跟蹤的程序段
set +x #關(guān)閉"-x"選項(xiàng)
set命令同樣可以使用上一節(jié)中介紹的調(diào)試鉤子—DEBUG函數(shù)來調(diào)用,這樣可以避免腳本交付使用時刪除這些調(diào)試語句的麻煩母谎,如以下腳本片段所示:
DEBUG set -x #啟動"-x"選項(xiàng)
要跟蹤的程序段
DEBUG set +x #關(guān)閉"-x"選項(xiàng)
四. 對"-x"選項(xiàng)的增強(qiáng)
"-x"執(zhí)行選項(xiàng)是目前最常用的跟蹤和調(diào)試shell腳本的手段旷太,但其輸出的調(diào)試信息僅限于進(jìn)行變量替換之后的每一條實(shí)際執(zhí)行的命令以及行首的一個"+"號提示符,居然連行號這樣的重要信息都沒有,對于復(fù)雜的shell腳本的調(diào)試來說供璧,還是非常的不方便存崖。幸運(yùn)的是,我們可以巧妙地利用shell內(nèi)置的一些環(huán)境變量來增強(qiáng)"-x"選項(xiàng)的輸出信息睡毒,下面先介紹幾個shell內(nèi)置的環(huán)境變量:
$LINENO
代表shell腳本的當(dāng)前行號来惧,類似于C語言中的內(nèi)置宏__LINE__
$FUNCNAME
函數(shù)的名字,類似于C語言中的內(nèi)置宏__func__,但宏__func__只能代表當(dāng)前所在的函數(shù)名演顾,而$FUNCNAME的功能更強(qiáng)大供搀,它是一個數(shù)組變量,其中包含了整個調(diào)用鏈上所有的函數(shù)的名字钠至,故變量${FUNCNAME[0]}代表shell腳本當(dāng)前正在執(zhí)行的函數(shù)的名字葛虐,而變量${FUNCNAME[1]}則代表調(diào)用函數(shù)${FUNCNAME[0]}的函數(shù)的名字,余者可以依此類推棉钧。
$PS4
主提示符變量$PS1和第二級提示符變量$PS2比較常見屿脐,但很少有人注意到第四級提示符變量$PS4的作用。我們知道使用“-x”執(zhí)行選項(xiàng)將會顯示shell腳本中每一條實(shí)際執(zhí)行過的命令宪卿,而$PS4的值將被顯示在“-x”選項(xiàng)輸出的每一條命令的前面的诵。在Bash Shell中,缺省的$PS4的值是"+"號佑钾。(現(xiàn)在知道為什么使用"-x"選項(xiàng)時西疤,輸出的命令前面有一個"+"號了吧?)休溶。
利用$PS4這一特性代赁,通過使用一些內(nèi)置變量來重定義$PS4的值,我們就可以增強(qiáng)"-x"選項(xiàng)的輸出信息兽掰。例如先執(zhí)行export PS4='+{$LINENO:${FUNCNAME[0]}} ', 然后再使用“-x”選項(xiàng)來執(zhí)行腳本管跺,就能在每一條實(shí)際執(zhí)行的命令前面顯示其行號以及所屬的函數(shù)名。
以下是一個存在bug的shell腳本的示例禾进,本文將用此腳本來示范如何用“-n”以及增強(qiáng)的“-x”執(zhí)行選項(xiàng)來調(diào)試shell腳本豁跑。這個腳本中定義了一個函數(shù)isRoot(),用于判斷當(dāng)前用戶是不是root用戶,如果不是泻云,則中止腳本的執(zhí)行
$ 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
首先執(zhí)行sh –n exp4.sh來進(jìn)行語法檢查艇拍,輸出如下:
$ sh –n exp4.sh
exp4.sh: line 6: syntax error near unexpected token `else'
exp4.sh: line 6: `? ? ? else'
發(fā)現(xiàn)了一個語法錯誤,通過仔細(xì)檢查第6行前后的命令宠纯,我們發(fā)現(xiàn)是第4行的if語句缺少then關(guān)鍵字引起的(寫慣了C程序的人很容易犯這個錯誤)卸夕。我們可以把第4行修改為if [ "$UID" -ne 0 ]; then來修正這個錯誤。再次運(yùn)行sh –n exp4.sh來進(jìn)行語法檢查婆瓜,沒有再報(bào)告錯誤快集。接下來就可以實(shí)際執(zhí)行這個腳本了贡羔,執(zhí)行結(jié)果如下:
$ sh exp4.sh
exp2.sh: line 11: [1: command not found
welcome root user
盡管腳本沒有語法錯誤了,在執(zhí)行時卻又報(bào)告了錯誤个初。錯誤信息還非常奇怪“[1: command not found”」院現(xiàn)在我們可以試試定制$PS4的值,并使用“-x”選項(xiàng)來跟蹤:
$ 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
從輸出結(jié)果中院溺,我們可以看到腳本實(shí)際被執(zhí)行的語句楣嘁,該語句的行號以及所屬的函數(shù)名也被打印出來,從中可以清楚的分析出腳本的執(zhí)行軌跡以及所調(diào)用的函數(shù)的內(nèi)部執(zhí)行情況珍逸。由于執(zhí)行時是第11行報(bào)錯逐虚,這是一個if語句,我們對比分析一下同為if語句的第4行的跟蹤結(jié)果:
+{4:isRoot} '[' 503 -ne 0 ']'
+{11:} '[1' -ne 0 ']'
可知由于第11行的[號后面缺少了一個空格谆膳,導(dǎo)致[號與緊挨它的變量$?的值1被shell解釋器看作了一個整體叭爱,并試著把這個整體視為一個命令來執(zhí)行,故有“[1: command not found”這樣的錯誤提示漱病。只需在[號后面插入一個空格就一切正常了买雾。
shell中還有其它一些對調(diào)試有幫助的內(nèi)置變量,比如在Bash Shell中還有BASH_SOURCE, BASH_SUBSHELL等一批對調(diào)試有幫助的內(nèi)置變量缨称,您可以通過man sh或man bash來查看凝果,然后根據(jù)您的調(diào)試目的,使用這些內(nèi)置變量來定制$PS4祝迂,從而達(dá)到增強(qiáng)“-x”選項(xiàng)的輸出信息的目的睦尽。
五. 總結(jié)
現(xiàn)在讓我們來總結(jié)一下調(diào)試shell腳本的過程:
首先使用“-n”選項(xiàng)檢查語法錯誤,然后使用“-x”選項(xiàng)跟蹤腳本的執(zhí)行型雳,使用“-x”選項(xiàng)之前当凡,別忘了先定制PS4變量的值來增強(qiáng)“-x”選項(xiàng)的輸出信息,至少應(yīng)該令其輸出行號信息(先執(zhí)行export PS4='+[$LINENO]'纠俭,更一勞永逸的辦法是將這條語句加到您用戶主目錄的.bash_profile文件中去)沿量,這將使你的調(diào)試之旅更輕松。也可以利用trap,調(diào)試鉤子等手段輸出關(guān)鍵調(diào)試信息冤荆,快速縮小排查錯誤的范圍朴则,并在腳本中使用“set -x”及“set +x”對某些代碼塊進(jìn)行重點(diǎn)跟蹤。這樣多種手段齊下钓简,相信您已經(jīng)可以比較輕松地抓出您的shell腳本中的臭蟲了乌妒。如果您的腳本足夠復(fù)雜,還需要更強(qiáng)的調(diào)試能力外邓,可以使用shell調(diào)試器bashdb撤蚊,這是一個類似于GDB的調(diào)試工具,可以完成對shell腳本的斷點(diǎn)設(shè)置损话,單步執(zhí)行侦啸,變量觀察等許多功能槽唾,使用bashdb對閱讀和理解復(fù)雜的shell腳本也會大有裨益。關(guān)于bashdb的安裝和使用光涂,不屬于本文范圍庞萍,您可參閱http://bashdb.sourceforge.net/上的文檔并下載試用。