本文將系統地介紹一些重要的shell腳本調試技術,希望能對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命令來查看中間結果會更方便一些。