Shell腳本調試技術

本文全面系統地介紹了shell腳本調試技術,包括使用echo, tee, trap等命令輸出關鍵信息,跟蹤變量的值,在腳本中植入調試鉤子,使用“-n”選項進行shell腳本的語法檢查, 使用“-x”選項實現shell腳本逐條語句的跟蹤,巧妙地利用shell的內置變量增強“-x”選項的輸出信息等。

曹 羽中(caoyuz@cn.ibm.com), 軟件工程師, IBM中國開發中心


一. 前言

shell編程在unix/linux世界中使用得非常廣泛,熟練掌握shell編程也是成為一名優秀的unix/linux開發者和系統管理員的必經之路。腳本調試的主要工作就是發現引發腳本錯誤的原因以及在腳本源代碼中定位發生錯誤的行,常用的手段包括分析輸出的錯誤信息,通過在腳本中加入調試語句,輸出調試信息來輔助診斷錯誤,利用調試工具等。但與其它高級語言相比,shell解釋器缺乏相應的調試機制和調試工具的支持,其輸出的錯誤信息又往往很不明確,初學者在調試腳本時,除了知道用echo語句輸出一些信息外,別無它法,而僅僅依賴于大量的加入echo語句來診斷錯誤,確實令人不勝其繁,故常見初學者抱怨shell腳本太難調試了。本文將系統地介紹一些重要的shell腳本調試技術,希望能對shell的初學者有所裨益。

本文的目標讀者是unix/linux環境下的開發人員,測試人員和系統管理員,要求讀者具有基本的shell編程知識。本文所使用范例在Bash3.1+Redhat Enterprise Server 4.0下測試通過,但所述調試技巧應也同樣適用于其它shell。

二. 在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

其輸出結果如下:

$ 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

在調試過程中,為了跟蹤某些變量的值,我們常常需要在shell腳本的許多地方插入相同的echo語句來打印相關變量的值,這種做法顯得煩瑣而笨拙。而通過捕獲DEBUG信號,我們只需要一條trap語句就可以完成對相關變量的全程跟蹤。

以下是一個通過捕獲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"

其輸出結果如下:

$ 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

從運行結果中可以清晰的看到每執行一條命令之后,相關變量的值的變化。同時,從運行結果中打印出來的行號來分析,可以看到整個腳本的執行軌跡,能夠判斷出哪些條件分支執行了,哪些條件分支沒有執行。

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

運行這個腳本,實際輸出的卻不是本機的ip地址,而是廣播地址,這時我們可以借助tee命令,輸出某些中間結果,將上述腳本片段修改為:

ipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'

| tee temp.txt | cut -d : -f3 | awk '{print $1}'`

echo $ipaddr

之后,將這段腳本再執行一遍,然后查看temp.txt文件的內容:

$ cat temp.txt

inet addr:192.168.0.1? Bcast:192.168.0.255? Mask:255.255.255.0

我們可以發現中間結果的第二列(列之間以:號分隔)才包含了IP地址,而在上面的腳本中使用cut命令截取了第三列,故我們只需將腳本中的cut -d : -f3改為cut -d : -f2即可得到正確的結果。

具體到上述的script例子,我們也許并不需要tee命令的幫助,比如我們可以分段執行由管道連接起來的各條命令并查看各命令的輸出結果來診斷錯誤,但在一些復雜的shell腳本中,這些由管道連接起來的命令可能又依賴于腳本中定義的一些其它變量,這時我們想要在提示符下來分段運行各條命令就會非常麻煩了,簡單地在管道之間插入一條tee命令來查看中間結果會更方便一些。

3. 使用"調試鉤子"

在C語言程序中,我們經常使用DEBUG宏來控制是否要輸出調試信息,在shell腳本中我們同樣可以使用這樣的機制,如下列代碼所示:

if [ “$DEBUG” = “true” ]; then

echo “debugging”? #此處可以輸出調試信息

fi

這樣的代碼塊通常稱之為“調試鉤子”或“調試塊”。在調試鉤子內部可以輸出任何您想輸出的調試信息,使用調試鉤子的好處是它是可以通過DEBUG變量來控制的,在腳本的開發調試階段,可以先執行export DEBUG=true命令打開調試鉤子,使其輸出調試信息,而在把腳本交付使用時,也無需再費事把腳本中的調試語句一一刪除。

如果在每一處需要輸出調試信息的地方均使用if語句來判斷DEBUG變量的值,還是顯得比較繁瑣,通過定義一個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函數中,會執行任何傳給它的命令,并且這個執行過程是可以通過DEBUG變量的值來控制的,我們可以把所有跟調試有關的命令都作為DEBUG函數的參數來調用,非常的方便。

三. 使用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 -x    #啟動"-x"選項

要跟蹤的程序段

set +x     #關閉"-x"選項

set命令同樣可以使用上一節中介紹的調試鉤子—DEBUG函數來調用,這樣可以避免腳本交付使用時刪除這些調試語句的麻煩,如以下腳本片段所示:

DEBUG set -x    #啟動"-x"選項

要跟蹤的程序段

DEBUG set +x    #關閉"-x"選項

四. 對"-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

首先執行sh –n exp4.sh來進行語法檢查,輸出如下:

$ sh –n exp4.sh

exp4.sh: line 6: syntax error near unexpected token `else'

exp4.sh: line 6: `? ? ? else'

發現了一個語法錯誤,通過仔細檢查第6行前后的命令,我們發現是第4行的if語句缺少then關鍵字引起的(寫慣了C程序的人很容易犯這個錯誤)。我們可以把第4行修改為if [ "$UID" -ne 0 ]; then來修正這個錯誤。再次運行sh –n exp4.sh來進行語法檢查,沒有再報告錯誤。接下來就可以實際執行這個腳本了,執行結果如下:

$ sh exp4.sh

exp2.sh: line 11: [1: command not found

welcome root user

盡管腳本沒有語法錯誤了,在執行時卻又報告了錯誤。錯誤信息還非常奇怪“[1: command not found”。現在我們可以試試定制$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

從輸出結果中,我們可以看到腳本實際被執行的語句,該語句的行號以及所屬的函數名也被打印出來,從中可以清楚的分析出腳本的執行軌跡以及所調用的函數的內部執行情況。由于執行時是第11行報錯,這是一個if語句,我們對比分析一下同為if語句的第4行的跟蹤結果:

+{4:isRoot} '[' 503 -ne 0 ']'

+{11:} '[1' -ne 0 ']'

可知由于第11行的[號后面缺少了一個空格,導致[號與緊挨它的變量$?的值1被shell解釋器看作了一個整體,并試著把這個整體視為一個命令來執行,故有“[1: command not found”這樣的錯誤提示。只需在[號后面插入一個空格就一切正常了。

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/上的文檔并下載試用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容

  • 建立一個腳本 Linux中有好多中不同的shell,但是通常我們使用bash (bourne again shel...
    某人在閱讀 2,023評論 0 0
  • 二. 在shell腳本中輸出調試信息 通過在程序中加入調試語句把一些關鍵地方或出錯的地方的相關信息顯示出來是最常見...
    很少更新了閱讀 828評論 0 1
  • linux資料總章2.1 1.0寫的不好抱歉 但是2.0已經改了很多 但是錯誤還是無法避免 以后資料會慢慢更新 大...
    數據革命閱讀 12,200評論 2 33
  • 一、shell腳本介紹 1.1 開頭(環境使用shebang機制) #!/bin/bash 必須寫在文件首行 符號...
    優果馥斯閱讀 3,321評論 0 1
  • 生活很平靜。 當磨難到來,找到你,不管它在社會層面上發生的概率是多少,對于你都是100% 你驚詫,你傷心,你努力,...
    yoyomok閱讀 150評論 0 0