1. 程式人生 > >shell指令碼除錯技術(轉)

shell指令碼除錯技術(轉)

本文全面系統地介紹了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指令碼中輸出除錯資訊
通過在程式中加入除錯語句把一些關鍵地方或出錯的地方的相關資訊顯示出來是最常見的除錯手段。Shell程式設計師通常使用echo(ksh程式設計師常使用 print)語句輸出資訊,但僅僅依賴echo語句的輸出跟蹤資訊很麻煩,除錯階段在指令碼中加入的大量的echo語句在產品交付時還得再費力一一刪除。針對這個問題,本節主要介紹一些如何方便有效的輸出除錯資訊的方法。

1. 使用trap命令
trap命令用於捕獲指定的訊號並執行預定義的命令。
其基本的語法是:
trap ‘command’ signal
其中signal是要捕獲的訊號,command是捕獲到指定的訊號之後,所要執行的命令。可以用kill –l命令看到系統中全部可用的訊號名,捕獲訊號後所執行的命令可以是任何一條或多條合法的shell語句,也可以是一個函式名。
shell指令碼在執行時,會產生三個所謂的“偽訊號”,(之所以稱之為“偽訊號”是因為這三個訊號是由shell產生的,而其它的訊號是由作業系統產生的),通過使用trap命令捕獲這三個“偽訊號”並輸出相關資訊對除錯非常有幫助。

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      [email protected]
     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/上的文件並下載試用。