在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

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

疑難排解

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

語法錯誤

一個普通的錯誤類型是語法。語法錯誤涉及到一些 shell 語法元素的拼寫錯誤。大多數(shù)情況下,這類錯誤 會導致 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

參看腳本內容,我們知道這個腳本執(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

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

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

:syntax on

丟失或意外的標記

另一個常見錯誤是忘記補全一個復合命令,比如說 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

結果是這樣的:

[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ā)生的事情真是相當有意思。我們記得, if 能夠接受一系列命令,并且會計算列表中最后一個命令的退出代碼。在我們的程序中,我們打算這個列表由 單個命令組成,即 [,測試的同義詞。這個 [ 命令把它后面的東西看作是一個參數(shù)列表。在我們這種情況下, 有三個參數(shù): $number,=,和 ]。由于刪除了分號,單詞 then 被添加到參數(shù)列表中,從語法上講, 這是合法的。隨后的 echo 命令也是合法的。它被解釋為命令列表中的另一個命令,if 將會計算命令的 退出代碼。接下來遇到單詞 else,但是它出局了,因為 shell 把它認定為一個 保留字(對于 shell 有特殊含義的單詞),而不是一個命令名,因此報告錯誤信息。

預料不到的展開

可能有這樣的錯誤,它們僅會間歇性地出現(xiàn)在一個腳本中。有時候這個腳本執(zhí)行正常,其它時間會失敗, 這是因為展開結果造成的。如果我們歸還我們丟掉的分號,并把 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.

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

[ $number = 1 ]

經過展開之后,number 變?yōu)榭罩担Y果就是這樣:

[  = 1 ]

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

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

[ "$number" = 1 ]

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

[ "" = 1 ]

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

邏輯錯誤

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

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

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

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

防錯編程

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

cd $dir_name
rm *

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

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

cd $dir_name && rm *

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

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

通常,當某種情況(比如上述問題)發(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ā)送一個錯誤說明信息到標準錯誤,然后腳本終止執(zhí)行,并用退出狀態(tài) 1 表明腳本執(zhí)行失敗。

驗證輸入

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

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

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

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

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

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

測試

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

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

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

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 命令來結束測試,從而防止執(zhí)行腳本其它部分的代碼。 這個需求會因腳本的設計不同而變化。

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

測試案例

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

  1. dir_name 包含一個已經存在的目錄的名字

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

  3. dir_name 為空

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

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

調試

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

找到問題區(qū)域

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

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ū)塊內的每條語句的開頭添加一個注釋符號,我們就阻止了這部分代碼的執(zhí)行。然后可以再次執(zhí)行測試, 來看看清除的代碼是否影響了錯誤的行為。

追蹤

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

一種追蹤方法涉及到在腳本中添加可以顯示程序執(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

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

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

[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

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

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

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

[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 選項關閉追蹤。這種技術可以用來檢查一個有錯誤的腳本的多個部分。

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

伴隨著追蹤,在腳本執(zhí)行的時候顯示變量的內容,以此知道腳本內部的工作狀態(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ù)值,并為其添加注釋,隨后利于其識別和清除。 當查看腳本中的循環(huán)和算術語句的時候,這種技術特別有用。

總結

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

拓展閱讀