鍍金池/ 教程/ Linux/ 疑難排解
網(wǎng)絡(luò)系統(tǒng)
打印
重定向
使用命令
位置參數(shù)
權(quán)限
文本處理
疑難排解
layout: book-zh title: 自定制 shell 提示符
查找文件
layout: book-zh title: vi 簡介
shell 環(huán)境
什么是 shell
編譯程序
鍵盤高級操作技巧
流程控制:case 分支
流程控制:if 分支結(jié)構(gòu)
layout: book-zh title: 軟件包管理
進程
存儲媒介
格式化輸出
編寫第一個 Shell 腳本
啟動一個項目
流程控制:while/until 循環(huán)
文件系統(tǒng)中跳轉(zhuǎn)
字符串和數(shù)字
讀取鍵盤輸入
歸檔和備份
探究操作系統(tǒng)
流程控制:for 循環(huán)
自頂向下設(shè)計
數(shù)組
操作文件和目錄
奇珍異寶
從 shell 眼中看世界
正則表達(dá)式

疑難排解

隨著我們的腳本變得越來越復(fù)雜,當(dāng)腳本運行錯誤,執(zhí)行結(jié)果出人意料的時候, 我們就應(yīng)該查看一下原因了。 在這一章中,我們將會看一些腳本中出現(xiàn)地常見錯誤類型,同時還會介紹幾個可以跟蹤和消除問題的有用技巧。

語法錯誤

一個普通的錯誤類型是語法。語法錯誤涉及到一些 shell 語法元素的拼寫錯誤。大多數(shù)情況下,這類錯誤 會導(dǎo)致 shell 拒絕執(zhí)行此腳本。

在以下討論中,我們將使用下面這個腳本,來說明常見的錯誤類型:

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

參看腳本內(nèi)容,我們知道這個腳本執(zhí)行成功了:

[me@linuxbox ~]$ trouble
Number is equal to 1.

丟失引號

如果我們編輯我們的腳本,并從跟隨第一個 echo 命令的參數(shù)中,刪除其末尾的雙引號:

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
    echo "Number is equal to 1.
else
    echo "Number is not equal to 1."
fi

觀察發(fā)生了什么:

[me@linuxbox ~]$ trouble
/home/me/bin/trouble: line 10: unexpected EOF while looking for
matching `"'
/home/me/bin/trouble: line 13: syntax error: unexpected end of file

這個腳本產(chǎn)生了兩個錯誤。有趣地是,所報告的行號不是引號被刪除的地方,而是程序中后面的文本行。 我們能知道為什么,如果我們跟隨丟失引號文本行之后的程序。bash 會繼續(xù)尋找右引號,直到它找到一個, 其就是這個緊隨第二個 echo 命令之后的引號。找到這個引號之后,bash 變得很困惑,并且 if 命令的語法 被破壞了,因為現(xiàn)在這個 fi 語句在一個用引號引起來的(但是開放的)字符串里面。

在冗長的腳本中,此類錯誤很難找到。使用帶有語法高亮的編輯器將會幫助查找錯誤。如果安裝了 vim 的完整版, 通過輸入下面的命令,可以使語法高亮生效:

:syntax on

丟失或意外的標(biāo)記

另一個常見錯誤是忘記補全一個復(fù)合命令,比如說 if 或者是 while。讓我們看一下,如果 我們刪除 if 命令中測試之后的分號,會出現(xiàn)什么情況:

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ] then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

結(jié)果是這樣的:

[me@linuxbox ~]$ trouble
/home/me/bin/trouble: line 9: syntax error near unexpected token
`else'
/home/me/bin/trouble: line 9: `else'

再次,錯誤信息指向一個錯誤,其出現(xiàn)的位置靠后于實際問題所在的文本行。所發(fā)生的事情真是相當(dāng)有意思。我們記得, if 能夠接受一系列命令,并且會計算列表中最后一個命令的退出代碼。在我們的程序中,我們打算這個列表由 單個命令組成,即 [,測試的同義詞。這個 [ 命令把它后面的東西看作是一個參數(shù)列表。在我們這種情況下, 有三個參數(shù): $number,=,和 ]。由于刪除了分號,單詞 then 被添加到參數(shù)列表中,從語法上講, 這是合法的。隨后的 echo 命令也是合法的。它被解釋為命令列表中的另一個命令,if 將會計算命令的 退出代碼。接下來遇到單詞 else,但是它出局了,因為 shell 把它認(rèn)定為一個 保留字(對于 shell 有特殊含義的單詞),而不是一個命令名,因此報告錯誤信息。

預(yù)料不到的展開

可能有這樣的錯誤,它們僅會間歇性地出現(xiàn)在一個腳本中。有時候這個腳本執(zhí)行正常,其它時間會失敗, 這是因為展開結(jié)果造成的。如果我們歸還我們丟掉的分號,并把 number 的數(shù)值更改為一個空變量,我們 可以示范一下:

#!/bin/bash
# trouble: script to demonstrate common errors
number=
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

運行這個做了修改的腳本,得到以下輸出:

[me@linuxbox ~]$ trouble
/home/me/bin/trouble: line 7: [: =: unary operator expected
Number is not equal to 1.

我們得到一個相當(dāng)神秘的錯誤信息,其后是第二個 echo 命令的輸出結(jié)果。這問題是由于 test 命令中 number 變量的展開結(jié)果造成的。當(dāng)此命令:

[ $number = 1 ]

經(jīng)過展開之后,number 變?yōu)榭罩?,結(jié)果就是這樣:

[  = 1 ]

這是無效的,所以就產(chǎn)生了錯誤。這個 = 操作符是一個二元操作符(它要求每邊都有一個數(shù)值),但是第一個數(shù)值是缺失的, 這樣 test 命令就期望用一個一元操作符(比如 -z)來代替。進一步說,因為 test 命令運行失敗了(由于錯誤), 這個 if 命令接收到一個非零退出代碼,因此執(zhí)行第二個 echo 命令。

通過為 test 命令中的第一個參數(shù)添加雙引號,可以更正這個問題:

[ "$number" = 1 ]

然后當(dāng)展開操作發(fā)生地時候,執(zhí)行結(jié)果將會是這樣:

[ "" = 1 ]

其得到了正確的參數(shù)個數(shù)。除了代表空字符串之外,引號應(yīng)該被用于這樣的場合,一個要展開 成多單詞字符串的數(shù)值,及其包含嵌入式空格的文件名。

邏輯錯誤

不同于語法錯誤,邏輯錯誤不會阻止腳本執(zhí)行。雖然腳本會正常運行,但是它不會產(chǎn)生期望的結(jié)果, 歸咎于腳本的邏輯問題。雖然有不計其數(shù)的可能的邏輯錯誤,但下面是一些在腳本中找到的最常見的 邏輯錯誤類型:

  1. 不正確的條件表達(dá)式。很容易編寫一個錯誤的 if/then/else 語句,并且執(zhí)行錯誤的邏輯。 有時候邏輯會被顛倒,或者是邏輯結(jié)構(gòu)不完整。

  2. “超出一個值”錯誤。當(dāng)編寫帶有計數(shù)器的循環(huán)語句的時候,為了計數(shù)在恰當(dāng)?shù)狞c結(jié)束,循環(huán)語句 可能要求從 0 開始計數(shù),而不是從 1 開始,這有可能會被忽視。這些類型的錯誤要不導(dǎo)致循環(huán)計數(shù)太多,而“超出范圍”, 要不就是過早的結(jié)束了一次迭代,從而錯過了最后一次迭代循環(huán)。

  3. 意外情況。大多數(shù)邏輯錯誤來自于程序碰到了程序員沒有預(yù)見到的數(shù)據(jù)或者情況。這也 可以包括出乎意料的展開,比如說一個包含嵌入式空格的文件名展開成多個命令參數(shù)而不是單個的文件名。

防錯編程

當(dāng)編程的時候,驗證假設(shè)非常重要。這意味著要仔細(xì)得計算腳本所使用的程序和命令的退出狀態(tài)。 這里有個實例,基于一個真實的故事。為了在一臺重要的服務(wù)器中執(zhí)行維護任務(wù),一位不幸的系統(tǒng)管理員寫了一個腳本。 這個腳本包含下面兩行代碼:

cd $dir_name
rm *

從本質(zhì)上來說,這兩行代碼沒有任何問題,只要是變量 dir_name 中存儲的目錄名字存在就可以。但是如果不是這樣會發(fā)生什么事情呢?在那種情況下,cd 命令會運行失敗, 腳本會繼續(xù)執(zhí)行下一行代碼,將會刪除當(dāng)前工作目錄中的所有文件。完成不是期望的結(jié)果! 由于這種設(shè)計策略,這個倒霉的管理員銷毀了服務(wù)器中的一個重要部分。

讓我們看一些能夠提高這個設(shè)計的方法。首先,在 cd 命令執(zhí)行成功之后,再運行 rm 命令,可能是明智的選擇。

cd $dir_name && rm *

這樣,如果 cd 命令運行失敗后,rm 命令將不會執(zhí)行。這樣比較好,但是仍然有可能未設(shè)置變量 dir_name 或其變量值為空,從而導(dǎo)致刪除了用戶家目錄下面的所有文件。這個問題也能夠避免,通過檢驗變量 dir_name 中包含的目錄名是否真正地存在:

[[ -d $dir_name ]] && cd $dir_name && rm *

通常,當(dāng)某種情況(比如上述問題)發(fā)生的時候,最好是終止腳本執(zhí)行,并對這種情況提示錯誤信息:

if [[ -d $dir_name ]]; then
    if cd $dir_name; then
        rm *
    else
        echo "cannot cd to '$dir_name'" >&2
        exit 1
    fi
else
    echo "no such directory: '$dir_name'" >&2
    exit 1
fi

這里,我們檢驗了兩種情況,一個名字,看看它是否為一個真正存在的目錄,另一個是 cd 命令是否執(zhí)行成功。 如果任一種情況失敗,就會發(fā)送一個錯誤說明信息到標(biāo)準(zhǔn)錯誤,然后腳本終止執(zhí)行,并用退出狀態(tài) 1 表明腳本執(zhí)行失敗。

驗證輸入

一個良好的編程習(xí)慣是如果一個程序可以接受輸入數(shù)據(jù),那么這個程序必須能夠應(yīng)對它所接受的任意數(shù)據(jù)。這 通常意味著必須非常仔細(xì)地篩選輸入數(shù)據(jù),以確保只有有效的輸入數(shù)據(jù)才能被程序用來做進一步地處理。在前面章節(jié) 中我們學(xué)習(xí) read 命令的時候,我們遇到過一個這樣的例子。一個腳本中包含了下面一條測試語句, 用來驗證一個選擇菜單:

[[ $REPLY =~ ^[0-3]$ ]]

這條測試語句非常明確。只有當(dāng)用戶輸入是一個位于 0 到 3 范圍內(nèi)(包括 0 和 3)的數(shù)字的時候, 這條語句才返回一個 0 退出狀態(tài)。而其它任何輸入概不接受。有時候編寫這類測試條件非常具有挑戰(zhàn)性, 但是為了能產(chǎn)出一個高質(zhì)量的腳本,付出還是必要的。

設(shè)計是時間的函數(shù)

當(dāng)我還是一名大學(xué)生,在學(xué)習(xí)工業(yè)設(shè)計的時候,一位明智的教授說過一個項目的設(shè)計程度是由 給定設(shè)計師的時間量來決定的。如果給你五分鐘來設(shè)計一款能夠 “殺死蒼蠅” 的產(chǎn)品,你會設(shè)計出一個蒼蠅拍。如果給你五個月的時間,你可能會制作出激光制導(dǎo)的 “反蒼蠅系統(tǒng)”。

同樣的原理適用于編程。有時候一個 “快速但粗糙” 的腳本就可以解決問題, 但這個腳本只能被其作者使用一次。這類腳本很常見,為了節(jié)省氣力也應(yīng)該被快速地開發(fā)出來。 所以這些腳本不需要太多的注釋和防錯檢查。相反,如果一個腳本打算用于生產(chǎn)使用,也就是說, 某個重要任務(wù)或者多個客戶會不斷地用到它,此時這個腳本就需要非常謹(jǐn)慎小心地開發(fā)了。

測試

在各類軟件開發(fā)中(包括腳本),測試是一個重要的環(huán)節(jié)。在開源世界中有一句諺語,“早發(fā)布,常發(fā)布”,這句諺語就反映出這個事實(測試的重要性)。 通過提早和經(jīng)常發(fā)布,軟件能夠得到更多曝光去使用和測試。經(jīng)驗表明如果在開發(fā)周期的早期發(fā)現(xiàn) bug,那么這些 bug 就越容易定位,而且越能低成本 的修復(fù)。

在之前的討論中,我們知道了如何使用 stubs 來驗證程序流程。在腳本開發(fā)的最初階段,它們是一項有價值的技術(shù) 來檢測我們的工作進度。

讓我們看一下上面的文件刪除問題,為了輕松測試,看看如何修改這些代碼。測試原本那個代碼片段將是危險的,因為它的目的是要刪除文件, 但是我們可以修改代碼,讓測試安全:

if [[ -d $dir_name ]]; then
    if cd $dir_name; then
        echo rm * # TESTING
    else
        echo "cannot cd to '$dir_name'" >&2
        exit 1
    fi
else
    echo "no such directory: '$dir_name'" >&2
    exit 1
fi
exit # TESTING

因為在滿足出錯條件的情況下代碼可以打印出有用信息,所以我們沒有必要再添加任何額外信息了。 最重要的改動是僅在 rm 命令之前放置了一個 echo 命令, 為的是把 rm 命令及其展開的參數(shù)列表打印出來,而不是執(zhí)行實際的 rm 命令語句。這個改動可以安全的執(zhí)行代碼。 在這段代碼的末尾,我們放置了一個 exit 命令來結(jié)束測試,從而防止執(zhí)行腳本其它部分的代碼。 這個需求會因腳本的設(shè)計不同而變化。

我們也在代碼中添加了一些注釋,用來標(biāo)記與測試相關(guān)的改動。當(dāng)測試完成之后,這些注釋可以幫助我們找到并刪除所有的更改。

測試案例

為了執(zhí)行有用的測試,開發(fā)和使用好的測試案例是很重要的。這個要求可以通過謹(jǐn)慎地選擇輸入數(shù)據(jù)或者運行邊緣案例和極端案例來完成。 在我們的代碼片段中(是非常簡單的代碼),我們想要知道在下面的三種具體情況下這段代碼是怎樣執(zhí)行的:

  1. dir_name 包含一個已經(jīng)存在的目錄的名字

  2. dir_name 包含一個不存在的目錄的名字

  3. dir_name 為空

通過執(zhí)行以上每一個測試條件,就達(dá)到了一個良好的測試覆蓋率。

正如設(shè)計,測試也是一個時間的函數(shù)。不是每一個腳本功能都需要做大量的測試。問題關(guān)鍵是確定什么功能是最重要的。因為 測試若發(fā)生故障會存在如此潛在的破壞性,所以我們的代碼片在設(shè)計和測試段期間都應(yīng)值得仔細(xì)推敲。

調(diào)試

如果測試暴露了腳本中的一個問題,那下一步就是調(diào)試了?!耙粋€問題”通常意味著在某種情況下,這個腳本的執(zhí)行 結(jié)果不是程序員所期望的結(jié)果。若是這種情況,我們需要仔細(xì)確認(rèn)這個腳本實際到底要完成什么任務(wù),和為什么要這樣做。 有時候查找 bug 要牽涉到許多監(jiān)測工作。一個設(shè)計良好的腳本會對查找錯誤有幫助。設(shè)計良好的腳本應(yīng)該具備防衛(wèi)能力, 能夠監(jiān)測異常條件,并能為用戶提供有用的反饋信息。 然而有時候,出現(xiàn)的問題相當(dāng)稀奇,出人意料,這時候就需要更多的調(diào)試技巧了。

找到問題區(qū)域

在一些腳本中,尤其是一些代碼比較長的腳本,有時候隔離腳本中與出現(xiàn)的問題相關(guān)的代碼區(qū)域?qū)Σ檎覇栴}很有效。 隔離的代碼區(qū)域并不總是真正的錯誤所在,但是隔離往往可以深入了解實際的錯誤原因??梢杂脕砀綦x代碼的一項 技巧是“添加注釋”。例如,我們的文件刪除代碼可以修改成這樣,從而決定注釋掉的這部分代碼是否導(dǎo)致了一個錯誤:

if [[ -d $dir_name ]]; then
    if cd $dir_name; then
        rm *
    else
        echo "cannot cd to '$dir_name'" >&2
        exit 1
    fi
# else
#
    echo "no such directory: '$dir_name'" >&2
#
    exit 1
fi

通過給腳本中的一個邏輯區(qū)塊內(nèi)的每條語句的開頭添加一個注釋符號,我們就阻止了這部分代碼的執(zhí)行。然后可以再次執(zhí)行測試, 來看看清除的代碼是否影響了錯誤的行為。

追蹤

在一個腳本中,錯誤往往是由意想不到的邏輯流導(dǎo)致的。也就是說,腳本中的一部分代碼或者從未執(zhí)行,或是以錯誤的順序, 或在錯誤的時間給執(zhí)行了。為了查看真實的程序流,我們使用一項叫做追蹤(tracing)的技術(shù)。

一種追蹤方法涉及到在腳本中添加可以顯示程序執(zhí)行位置的提示性信息。我們可以添加提示信息到我們的代碼片段中:

echo "preparing to delete files" >&2
if [[ -d $dir_name ]]; then
    if cd $dir_name; then
echo "deleting files" >&2
        rm *
    else
        echo "cannot cd to '$dir_name'" >&2
        exit 1
    fi
else
    echo "no such directory: '$dir_name'" >&2
    exit 1
fi
echo "file deletion complete" >&2

我們把提示信息輸出到標(biāo)準(zhǔn)錯誤輸出,讓其從標(biāo)準(zhǔn)輸出中分離出來。我們也沒有縮進包含提示信息的語句,這樣 想要刪除它們的時候,能比較容易找到它們。

當(dāng)這個腳本執(zhí)行的時候,就可能看到文件刪除操作已經(jīng)完成了:

[me@linuxbox ~]$ deletion-script
preparing to delete files
deleting files
file deletion complete
[me@linuxbox ~]$

bash 還提供了一種名為追蹤的方法,這種方法可通過 -x 選項和 set 命令加上 -x 選項兩種途徑實現(xiàn)。 拿我們之前的 trouble 腳本為例,給該腳本的第一行語句添加 -x 選項,我們就能追蹤整個腳本。

#!/bin/bash -x
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

當(dāng)腳本執(zhí)行后,輸出結(jié)果看起來像這樣:

[me@linuxbox ~]$ trouble
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.

追蹤生效后,我們看到腳本命令展開后才執(zhí)行。行首的加號表明追蹤的跡象,使其與常規(guī)輸出結(jié)果區(qū)分開來。 加號是追蹤輸出的默認(rèn)字符。它包含在 PS4(提示符4)shell 變量中??梢哉{(diào)整這個變量值讓提示信息更有意義。 這里,我們修改該變量的內(nèi)容,讓其包含腳本中追蹤執(zhí)行到的當(dāng)前行的行號。注意這里必須使用單引號是為了防止變量展開,直到 提示符真正使用的時候,就不需要了。

[me@linuxbox ~]$ export PS4='$LINENO + '
[me@linuxbox ~]$ trouble
5 + number=1
7 + '[' 1 = 1 ']'
8 + echo 'Number is equal to 1.'
Number is equal to 1.

我們可以使用 set 命令加上 -x 選項,為腳本中的一塊選擇區(qū)域,而不是整個腳本啟用追蹤。

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
set -x # Turn on tracing
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi
set +x # Turn off tracing

我們使用 set 命令加上 -x 選項來啟動追蹤,+x 選項關(guān)閉追蹤。這種技術(shù)可以用來檢查一個有錯誤的腳本的多個部分。

執(zhí)行時檢查數(shù)值

伴隨著追蹤,在腳本執(zhí)行的時候顯示變量的內(nèi)容,以此知道腳本內(nèi)部的工作狀態(tài),往往是很用的。 使用額外的 echo 語句通常會奏效。

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
echo "number=$number" # DEBUG
set -x # Turn on tracing
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi
set +x # Turn off tracing

在這個簡單的示例中,我們只是顯示變量 number 的數(shù)值,并為其添加注釋,隨后利于其識別和清除。 當(dāng)查看腳本中的循環(huán)和算術(shù)語句的時候,這種技術(shù)特別有用。

總結(jié)

在這一章中,我們僅僅看了幾個在腳本開發(fā)期間會出現(xiàn)的問題。當(dāng)然,還有很多。這章中描述的技術(shù)對查找 大多數(shù)的常見錯誤是有效的。調(diào)試是一種藝術(shù),可以通過開發(fā)經(jīng)驗,在知道如何避免錯誤(整個開發(fā)過程中不斷測試) 以及在查找 bug(有效利用追蹤)兩方面都會得到提升。

拓展閱讀