shell編程在unix/linux世界中使用得非常廣泛,熟練掌握shell編程也是成為一名優(yōu)秀的unix/linux開發(fā)者和系統(tǒng)管理員的必經(jīng)之路,。腳本調(diào)試的主要工作就是發(fā)現(xiàn)引發(fā)腳本錯(cuò)誤的原因以及在腳本源代碼中定位發(fā)生錯(cuò)誤的行,,常用的手段包括分析輸出的錯(cuò)誤信息,通過在腳本中加入調(diào)試語句,,輸出調(diào)試信息來輔助診斷錯(cuò)誤,,利用調(diào)試工具等。但與其它高級語言相比,,shell解釋器缺乏相應(yīng)的調(diào)試機(jī)制和調(diào)試工具的支持,,其輸出的錯(cuò)誤信息又往往很不明確,初學(xué)者在調(diào)試腳本時(shí),,除了知道用echo語句輸出一些信息外,,別無它法,而僅僅依賴于大量的加入echo語句來診斷錯(cuò)誤,確實(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。
通過在程序中加入調(diào)試語句把一些關(guān)鍵地方或出錯(cuò)的地方的相關(guān)信息顯示出來是最常見的調(diào)試手段,。Shell程序員通常使用echo(ksh程序員常使用print)語句輸出信息,,但僅僅依賴echo語句的輸出跟蹤信息很麻煩,調(diào)試階段在腳本中加入的大量的echo語句在產(chǎn)品交付時(shí)還得再費(fèi)力一一刪除,。針對這個(gè)問題,,本節(jié)主要介紹一些如何方便有效的輸出調(diào)試信息的方法。
trap命令用于捕獲指定的信號并執(zhí)行預(yù)定義的命令,。
其基本的語法是:
trap 'command' signal
其中signal是要捕獲的信號,,command是捕獲到指定的信號之后,所要執(zhí)行的命令,??梢杂胟ill –l命令看到系統(tǒng)中全部可用的信號名,捕獲信號后所執(zhí)行的命令可以是任何一條或多條合法的shell語句,,也可以是一個(gè)函數(shù)名,。
shell腳本在執(zhí)行時(shí),會產(chǎn)生三個(gè)所謂的“偽信號”,,(之所以稱之為“偽信號”是因?yàn)檫@三個(gè)信號是由shell產(chǎn)生的,而其它的信號是由操作系統(tǒng)產(chǎn)生的),,通過使用trap命令捕獲這三個(gè)“偽信號”并輸出相關(guān)信息對調(diào)試非常有幫助,。
表 1. shell偽信號
信號名 | 何時(shí)產(chǎn)生 |
---|---|
EXIT | 從一個(gè)函數(shù)中退出或整個(gè)腳本執(zhí)行完畢 |
ERR | 當(dāng)一條命令返回非零狀態(tài)時(shí)(代表命令執(zhí)行不成功) |
DEBUG | 腳本中每一條命令執(zhí)行之前 |
通過捕獲EXIT信號,我們可以在shell腳本中止執(zhí)行或從函數(shù)中退出時(shí),,輸出某些想要跟蹤的變量的值,,并由此來判斷腳本的執(zhí)行狀態(tài)以及出錯(cuò)原因,其使用方法是:
trap 'command' EXIT 或 trap 'command' 0
通過捕獲ERR信號,我們可以方便的追蹤執(zhí)行不成功的命令或函數(shù),并輸出相關(guān)的調(diào)試信息,,以下是一個(gè)捕獲ERR信號的示例程序,,其中的$LINENO是一個(gè)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)變量的全程跟蹤。
以下是一個(gè)通過捕獲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)變量的值的變化,。同時(shí),從運(yùn)行結(jié)果中打印出來的行號來分析,,可以看到整個(gè)腳本的執(zhí)行軌跡,,能夠判斷出哪些條件分支執(zhí)行了,哪些條件分支沒有執(zhí)行,。
在shell腳本中管道以及輸入輸出重定向使用得非常多,,在管道的作用下,一些命令的執(zhí)行結(jié)果直接成為了下一條命令的輸入,。如果我們發(fā)現(xiàn)由管道連接起來的一批命令的執(zhí)行結(jié)果并非如預(yù)期的那樣,,就需要逐步檢查各條命令的執(zhí)行結(jié)果來判斷問題出在哪兒,但因?yàn)槭褂昧斯艿?,這些中間結(jié)果并不會顯示在屏幕上,,給調(diào)試帶來了困難,此時(shí)我們就可以借助于tee命令了,。
tee命令會從標(biāo)準(zhǔn)輸入讀取數(shù)據(jù),,將其內(nèi)容輸出到標(biāo)準(zhǔn)輸出設(shè)備,同時(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鍵的左邊那個(gè)鍵)括起來的,。 echo $ipaddr |
運(yùn)行這個(gè)腳本,實(shí)際輸出的卻不是本機(jī)的ip地址,,而是廣播地址,這時(shí)我們可以借助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é)果來診斷錯(cuò)誤,,但在一些復(fù)雜的shell腳本中,這些由管道連接起來的命令可能又依賴于腳本中定義的一些其它變量,,這時(shí)我們想要在提示符下來分段運(yùn)行各條命令就會非常麻煩了,,簡單地在管道之間插入一條tee命令來查看中間結(jié)果會更方便一些。
在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)試信息,,而在把腳本交付使用時(shí),也無需再費(fèi)事把腳本中的調(diào)試語句一一刪除,。
如果在每一處需要輸出調(diào)試信息的地方均使用if語句來判斷DEBUG變量的值,,還是顯得比較繁瑣,通過定義一個(gè)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í)行任何傳給它的命令,并且這個(gè)執(zhí)行過程是可以通過DEBUG變量的值來控制的,,我們可以把所有跟調(diào)試有關(guān)的命令都作為DEBUG函數(shù)的參數(shù)來調(diào)用,,非常的方便。
上一節(jié)所述的調(diào)試手段是通過修改shell腳本的源代碼,,令其輸出相關(guān)的調(diào)試信息來定位錯(cuò)誤的,,那有沒有不修改源代碼來調(diào)試shell腳本的方法呢?答案就是使用shell的執(zhí)行選項(xiàng),,本節(jié)將介紹一些常用選項(xiàng)的用法:
-n 只讀取shell腳本,,但不實(shí)際執(zhí)行
-x 進(jìn)入跟蹤方式,顯示所執(zhí)行的每一條命令
-c "string" 從strings中讀取命令
“-n”可用于測試shell腳本是否存在語法錯(cuò)誤,,但不會實(shí)際執(zhí)行命令,。在shell腳本編寫完成之后,實(shí)際執(zhí)行之前,,首先使用“-n”選項(xiàng)來測試腳本是否存在語法錯(cuò)誤是一個(gè)很好的習(xí)慣,。因?yàn)槟承﹕hell腳本在執(zhí)行時(shí)會對系統(tǒng)環(huán)境產(chǎn)生影響,比如生成或移動(dòng)文件等,,如果在實(shí)際執(zhí)行才發(fā)現(xiàn)語法錯(cuò)誤,,您不得不手工做一些系統(tǒng)環(huán)境的恢復(fù)工作才能繼續(xù)測試這個(gè)腳本,。
“-c”選項(xiàng)使shell解釋器從一個(gè)字符串中而不是從一個(gè)文件中讀取并執(zhí)行shell命令,。當(dāng)需要臨時(shí)測試一小段腳本的執(zhí)行結(jié)果時(shí),可以使用這個(gè)選項(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í)行的每一個(gè)命令行顯示出來,,并且在行首顯示一個(gè)"+"號,。 "+"號后面顯示的是經(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)除了可以在啟動(dòng)shell時(shí)指定外,,亦可在腳本中用set命令來指定,。 "set -參數(shù)"表示啟用某選項(xiàng),"set +參數(shù)"表示關(guān)閉某選項(xiàng),。有時(shí)候我們并不需要在啟動(dòng)時(shí)用"-x"選項(xiàng)來跟蹤所有的命令行,,這時(shí)我們可以在腳本中使用set命令,如以下腳本片段所示:
set -x #啟動(dòng)"-x"選項(xiàng) 要跟蹤的程序段 set +x #關(guān)閉"-x"選項(xiàng) |
set命令同樣可以使用上一節(jié)中介紹的調(diào)試鉤子—DEBUG函數(shù)來調(diào)用,,這樣可以避免腳本交付使用時(shí)刪除這些調(diào)試語句的麻煩,,如以下腳本片段所示:
DEBUG set -x #啟動(dòng)"-x"選項(xiàng) 要跟蹤的程序段 DEBUG set +x #關(guān)閉"-x"選項(xiàng) |
"-x"執(zhí)行選項(xiàng)是目前最常用的跟蹤和調(diào)試shell腳本的手段,但其輸出的調(diào)試信息僅限于進(jìn)行變量替換之后的每一條實(shí)際執(zhí)行的命令以及行首的一個(gè)"+"號提示符,,居然連行號這樣的重要信息都沒有,,對于復(fù)雜的shell腳本的調(diào)試來說,還是非常的不方便,。幸運(yùn)的是,,我們可以巧妙地利用shell內(nèi)置的一些環(huán)境變量來增強(qiáng)"-x"選項(xiàng)的輸出信息,下面先介紹幾個(gè)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)大,,它是一個(gè)數(shù)組變量,,其中包含了整個(gè)調(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)時(shí),輸出的命令前面有一個(gè)"+"號了吧,?),。
利用$PS4這一特性,通過使用一些內(nèi)置變量來重定義$PS4的值,,我們就可以增強(qiáng)"-x"選項(xiàng)的輸出信息,。例如先執(zhí)行export PS4='+{$LINENO:${FUNCNAME[0]}} ', 然后再使用“-x”選項(xiàng)來執(zhí)行腳本,就能在每一條實(shí)際執(zhí)行的命令前面顯示其行號以及所屬的函數(shù)名,。
以下是一個(gè)存在bug的shell腳本的示例,,本文將用此腳本來示范如何用“-n”以及增強(qiáng)的“-x”執(zhí)行選項(xiàng)來調(diào)試shell腳本。這個(gè)腳本中定義了一個(gè)函數(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)了一個(gè)語法錯(cuò)誤,,通過仔細(xì)檢查第6行前后的命令,,我們發(fā)現(xiàn)是第4行的if語句缺少then關(guān)鍵字引起的(寫慣了C程序的人很容易犯這個(gè)錯(cuò)誤)。我們可以把第4行修改為if [ "$UID" -ne 0 ]; then來修正這個(gè)錯(cuò)誤,。再次運(yùn)行sh –n exp4.sh來進(jìn)行語法檢查,,沒有再報(bào)告錯(cuò)誤。接下來就可以實(shí)際執(zhí)行這個(gè)腳本了,,執(zhí)行結(jié)果如下:
$ sh exp4.sh exp2.sh: line 11: [1: command not found welcome root user |
盡管腳本沒有語法錯(cuò)誤了,,在執(zhí)行時(shí)卻又報(bào)告了錯(cuò)誤。錯(cuò)誤信息還非常奇怪“[1: command not found”?,F(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í)行時(shí)是第11行報(bào)錯(cuò),,這是一個(gè)if語句,,我們對比分析一下同為if語句的第4行的跟蹤結(jié)果:
+{4:isRoot} '[' 503 -ne 0 ']' +{11:} '[1' -ne 0 ']' |
可知由于第11行的[號后面缺少了一個(gè)空格,導(dǎo)致[號與緊挨它的變量$?的值1被shell解釋器看作了一個(gè)整體,,并試著把這個(gè)整體視為一個(gè)命令來執(zhí)行,,故有“[1: command not found”這樣的錯(cuò)誤提示。只需在[號后面插入一個(gè)空格就一切正常了,。
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)的輸出信息的目的。
現(xiàn)在讓我們來總結(jié)一下調(diào)試shell腳本的過程:
首先使用“-n”選項(xiàng)檢查語法錯(cuò)誤,,然后使用“-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)試信息,,快速縮小排查錯(cuò)誤的范圍,并在腳本中使用“set -x”及“set +x”對某些代碼塊進(jìn)行重點(diǎn)跟蹤,。這樣多種手段齊下,,相信您已經(jīng)可以比較輕松地抓出您的shell腳本中的臭蟲了。如果您的腳本足夠復(fù)雜,,還需要更強(qiáng)的調(diào)試能力,,可以使用shell調(diào)試器bashdb,這是一個(gè)類似于GDB的調(diào)試工具,,可以完成對shell腳本的斷點(diǎn)設(shè)置,,單步執(zhí)行,變量觀察等許多功能,,使用bashdb對閱讀和理解復(fù)雜的shell腳本也會大有裨益,。關(guān)于bashdb的安裝和使用,不屬于本文范圍,,您可參閱http://bashdb./上的文檔并下載試用,。
- 請?jiān)L問:
GNU 的 bash 主頁
- 請下載和試用
Shell調(diào)試器bashdb
曹羽中,在北京航空航天大學(xué)獲得計(jì)算機(jī)軟件與理論專業(yè)的碩士學(xué)位,,具有數(shù)年的 unix 環(huán)境下的 C 語言,Java,數(shù)據(jù)庫以及電信計(jì)費(fèi)軟件的開發(fā)經(jīng)驗(yàn),,他的技術(shù)興趣還包括 OSGi 和搜索技術(shù)。他目前在IBM中國系統(tǒng)與科技實(shí)驗(yàn)室從事系統(tǒng)管理軟件的開發(fā)工作,可以通過 [email protected]與他聯(lián)系,。