天天看點

【轉】Linux Shell腳本調試技術Shell腳本調試技術

本文轉載自:https://www.ibm.com/developerworks/cn/linux/l-cn-shell-debug/

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

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

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

通過在程式中加入調試語句把一些關鍵地方或出錯的地方的相關資訊顯示出來是最常見的調試手段。Shell程式員通常使用echo(ksh程式員常使用print)語句輸出資訊,但僅僅依賴echo語句的輸出跟蹤資訊很麻煩,調試階段在腳本中加入的大量的echo語句在産品傳遞時還得再費力一一删除。針對這個問題,本節主要介紹一些如何友善有效的輸出調試資訊的方法。

trap指令用于捕獲指定的信号并執行預定義的指令。

其基本的文法是:

trap 'command' signal

其中signal是要捕獲的信号,command是捕獲到指定的信号之後,所要執行的指令。可以用kill –l指令看到系統中全部可用的信号名,捕獲信号後所執行的指令可以是任何一條或多條合法的shell語句,也可以是一個函數名。

shell腳本在執行時,會産生三個所謂的“僞信号”,(之是以稱之為“僞信号”是因為這三個信号是由shell産生的,而其它的信号是由作業系統産生的),通過使用trap指令捕獲這三個“僞信号”并輸出相關資訊對調試非常有幫助。

信号名

何時産生

EXIT

從一個函數中退出或整個腳本執行完畢

ERR

當一條指令傳回非零狀态時(代表指令執行不成功)

DEBUG

腳本中每一條指令執行之前

通過捕獲EXIT信号,我們可以在shell腳本中止執行或從函數中退出時,輸出某些想要跟蹤的變量的值,并由此來判斷腳本的執行狀态以及出錯原因,其使用方法是:

trap 'command' EXIT 或 trap 'command' 0

通過捕獲ERR信号,我們可以友善的追蹤執行不成功的指令或函數,并輸出相關的調試資訊,以下是一個捕獲ERR信号的示例程式,其中的$LINENO是一個shell的内置變量,代表shell腳本的目前行号。

其輸出結果如下:

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

以下是一個通過捕獲DEBUG信号來跟蹤變量的示例程式:

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

在shell腳本中管道以及輸入輸出重定向使用得非常多,在管道的作用下,一些指令的執行結果直接成為了下一條指令的輸入。如果我們發現由管道連接配接起來的一批指令的執行結果并非如預期的那樣,就需要逐漸檢查各條指令的執行結果來判斷問題出在哪兒,但因為使用了管道,這些中間結果并不會顯示在螢幕上,給調試帶來了困難,此時我們就可以借助于tee指令了。

tee指令會從标準輸入讀取資料,将其内容輸出到标準輸出裝置,同時又可将内容儲存成檔案。例如有如下的腳本片段,其作用是擷取本機的ip位址:

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

之後,将這段腳本再執行一遍,然後檢視temp.txt檔案的内容:

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

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

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

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

如果在每一處需要輸出調試資訊的地方均使用if語句來判斷DEBUG變量的值,還是顯得比較繁瑣,通過定義一個DEBUG函數可以使植入調試鈎子的過程更簡潔友善,如下面代碼所示:

在上面所示的DEBUG函數中,會執行任何傳給它的指令,并且這個執行過程是可以通過DEBUG變量的值來控制的,我們可以把所有跟調試有關的指令都作為DEBUG函數的參數來調用,非常的友善。

上一節所述的調試手段是通過修改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”選項來執行它:

在上面的結果中,前面有“+”号的行是shell腳本實際執行的指令,前面有“++”号的行是執行trap機制中指定的指令,其它的行則是輸出資訊。

shell的執行選項除了可以在啟動shell時指定外,亦可在腳本中用set指令來指定。 "set -參數"表示啟用某選項,"set +參數"表示關閉某選項。有時候我們并不需要在啟動時用"-x"選項來跟蹤所有的指令行,這時我們可以在腳本中使用set指令,如以下腳本片段所示:

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

"-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使用者,如果不是,則中止腳本的執行

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

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

盡管腳本沒有文法錯誤了,在執行時卻又報告了錯誤。錯誤資訊還非常奇怪“[1: command not found”。現在我們可以試試定制$PS4的值,并使用“-x”選項來跟蹤:

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

可知由于第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/上的文檔并下載下傳試用。