在上一章中,我們遇到一個問題。怎樣使我們的報告生成器腳本能適應(yīng)運(yùn)行此腳本的用戶的權(quán)限? 這個問題的解決方案要求我們能找到一種方法,在腳本中基于測試條件結(jié)果,來“改變方向”。 用編程術(shù)語表達(dá),就是我們需要程序可以分支。讓我們考慮一個簡單的用偽碼表示的邏輯實例, 偽碼是一種模擬的計算機(jī)語言,為的是便于人們理解:
X=5
If X = 5, then:
Say “X equals 5.”
Otherwise:
Say “X is not equal to 5.”
這就是一個分支的例子。根據(jù)條件,“Does X = 5?” 做一件事情,“Say X equals 5,” 否則,做另一件事情,“Say X is not equal to 5.”
使用 shell,我們可以編碼上面的邏輯,如下所示:
x=5
if [ $x = 5 ]; then
echo "x equals 5."
else
echo "x does not equal 5."
fi
或者我們可以直接在命令行中輸入以上代碼(略有縮短):
[me@linuxbox ~]$ x=5
[me@linuxbox ~]$ if [ $x = 5 ]; then echo "equals 5"; else echo "does
not equal 5"; fi
equals 5
[me@linuxbox ~]$ x=0
[me@linuxbox ~]$ if [ $x = 5 ]; then echo "equals 5"; else echo "does
not equal 5"; fi
does not equal 5
在這個例子中,我們執(zhí)行了兩次這個命令。第一次是,把 x 的值設(shè)置為5,從而導(dǎo)致輸出字符串“equals 5”, 第二次是,把 x 的值設(shè)置為0,從而導(dǎo)致輸出字符串“does not equal 5”。
這個 if 語句語法如下:
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
這里的 commands 是指一系列命令。第一眼看到會有點兒困惑。但是在我們弄清楚這些語句之前,我們 必須看一下 shell 是如何評判一個命令的成功與失敗的。
當(dāng)命令執(zhí)行完畢后,命令(包括我們編寫的腳本和 shell 函數(shù))會給系統(tǒng)發(fā)送一個值,叫做退出狀態(tài)。 這個值是一個 0 到 255 之間的整數(shù),說明命令執(zhí)行成功或是失敗。按照慣例,一個零值說明成功,其它所有值說明失敗。 Shell 提供了一個參數(shù),我們可以用它檢查退出狀態(tài)。用具體實例看一下:
[me@linuxbox ~]$ ls -d /usr/bin
/usr/bin
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ ls -d /bin/usr
ls: cannot access /bin/usr: No such file or directory
[me@linuxbox ~]$ echo $?
2
在這個例子中,我們執(zhí)行了兩次 ls 命令。第一次,命令執(zhí)行成功。如果我們顯示參數(shù)$?
的值,我們
看到它是零。我們第二次執(zhí)行 ls 命令的時候,產(chǎn)生了一個錯誤,并再次查看參數(shù)$?
。這次它包含一個
數(shù)字 2,表明這個命令遇到了一個錯誤。有些命令使用不同的退出值,來診斷錯誤,而許多命令當(dāng)
它們執(zhí)行失敗的時候,會簡單地退出并發(fā)送一個數(shù)字1。手冊頁中經(jīng)常會包含一章標(biāo)題為“退出狀態(tài)”的內(nèi)容,
描述了使用的代碼。然而,一個零總是表明成功。
這個 shell 提供了兩個極其簡單的內(nèi)部命令,它們不做任何事情,除了以一個零或1退出狀態(tài)來終止執(zhí)行。 True 命令總是執(zhí)行成功,而 false 命令總是執(zhí)行失敗:
[me@linuxbox~]$ true
[me@linuxbox~]$ echo $?
0
[me@linuxbox~]$ false
[me@linuxbox~]$ echo $?
1
我們能夠使用這些命令,來看一下 if 語句是怎樣工作的。If 語句真正做的事情是計算命令執(zhí)行成功或失敗:
[me@linuxbox ~]$ if true; then echo "It's true."; fi
It's true.
[me@linuxbox ~]$ if false; then echo "It's true."; fi
[me@linuxbox ~]$
當(dāng) if 之后的命令執(zhí)行成功的時候,命令 echo "It's true." 將會執(zhí)行,否則此命令不執(zhí)行。 如果 if 之后跟隨一系列命令,則將計算列表中的最后一個命令:
[me@linuxbox ~]$ if false; true; then echo "It's true."; fi
It's true.
[me@linuxbox ~]$ if true; false; then echo "It's true."; fi
[me@linuxbox ~]$
3
到目前為止,經(jīng)常與 if 一塊使用的命令是 test。這個 test 命令執(zhí)行各種各樣的檢查與比較。 它有兩種等價模式:
test expression
比較流行的格式是:
[ expression ]
這里的 expression 是一個表達(dá)式,其執(zhí)行結(jié)果是 true 或者是 false。當(dāng)表達(dá)式為真時,這個 test 命令返回一個零 退出狀態(tài),當(dāng)表達(dá)式為假時,test 命令退出狀態(tài)為1。
以下表達(dá)式被用來計算文件狀態(tài):
表達(dá)式 | 如果為真 |
---|---|
file1 -ef file2 | file1 和 file2 擁有相同的索引號(通過硬鏈接兩個文件名指向相同的文件)。 |
file1 -nt file2 | file1新于 file2。 |
file1 -ot file2 | file1早于 file2。 |
-b file | file 存在并且是一個塊(設(shè)備)文件。 |
-c file | file 存在并且是一個字符(設(shè)備)文件。 |
-d file | file 存在并且是一個目錄。 |
-e file | file 存在。 |
-f file | file 存在并且是一個普通文件。 |
-g file | file 存在并且設(shè)置了組 ID。 |
-G file | file 存在并且由有效組 ID 擁有。 |
-k file | file 存在并且設(shè)置了它的“sticky bit”。 |
-L file | file 存在并且是一個符號鏈接。 |
-O file | file 存在并且由有效用戶 ID 擁有。 |
-p file | file 存在并且是一個命名管道。 |
-r file | file 存在并且可讀(有效用戶有可讀權(quán)限)。 |
-s file | file 存在且其長度大于零。 |
-S file | file 存在且是一個網(wǎng)絡(luò) socket。 |
-t fd | fd 是一個定向到終端/從終端定向的文件描述符 。 這可以被用來決定是否重定向了標(biāo)準(zhǔn)輸入/輸出錯誤。 |
-u file | file 存在并且設(shè)置了 setuid 位。 |
-w file | file 存在并且可寫(有效用戶擁有可寫權(quán)限)。 |
-x file | file 存在并且可執(zhí)行(有效用戶有執(zhí)行/搜索權(quán)限)。 |
這里我們有一個腳本說明了一些文件表達(dá)式:
#!/bin/bash
# test-file: Evaluate the status of a file
FILE=~/.bashrc
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
exit 1
fi
exit
這個腳本會計算賦值給常量 FILE 的文件,并顯示計算結(jié)果。對于此腳本有兩點需要注意。第一個,
在表達(dá)式中參數(shù)$FILE
是怎樣被引用的。引號并不是必需的,但這是為了防范空參數(shù)。如果$FILE
的參數(shù)展開
是一個空值,就會導(dǎo)致一個錯誤(操作符將會被解釋為非空的字符串而不是操作符)。用引號把參數(shù)引起來就
確保了操作符之后總是跟隨著一個字符串,即使字符串為空。第二個,注意腳本末尾的 exit 命令。
這個 exit 命令接受一個單獨的,可選的參數(shù),其成為腳本的退出狀態(tài)。當(dāng)不傳遞參數(shù)時,退出狀態(tài)默認(rèn)為零。
以這種方式使用 exit 命令,則允許此腳本提示失敗如果 $FILE
展開成一個不存在的文件名。這個 exit 命令
出現(xiàn)在腳本中的最后一行,是一個當(dāng)一個腳本“運(yùn)行到最后”(到達(dá)文件末尾),不管怎樣,
默認(rèn)情況下它以退出狀態(tài)零終止。
類似地,通過帶有一個整數(shù)參數(shù)的 return 命令,shell 函數(shù)可以返回一個退出狀態(tài)。如果我們打算把 上面的腳本轉(zhuǎn)變?yōu)橐粋€ shell 函數(shù),為了在更大的程序中包含此函數(shù),我們用 return 語句來代替 exit 命令, 則得到期望的行為:
test_file () {
# test-file: Evaluate the status of a file
FILE=~/.bashrc
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
return 1
fi
}
以下表達(dá)式用來計算字符串:
表達(dá)式 | 如果為真... |
---|---|
string | string 不為 null。 |
-n string | 字符串 string 的長度大于零。 |
-z string | 字符串 string 的長度為零。 |
string1 = string2 string1 == string2 |
string1 和 string2 相同. 單或雙等號都可以,不過雙等號更受歡迎。 |
string1 != string2 | string1 和 string2 不相同。 |
string1 > string2 | sting1 排列在 string2 之后。 |
string1 | string1 排列在 string2 之前。 |
警告:這個 > 和 <表達(dá)式操作符必須用引號引起來(或者是用反斜杠轉(zhuǎn)義), 當(dāng)與 test 一塊使用的時候。如果不這樣,它們會被 shell 解釋為重定向操作符,造成潛在地破壞結(jié)果。 同時也要注意雖然 bash 文檔聲明排序遵從當(dāng)前語系的排列規(guī)則,但并不這樣。將來的 bash 版本,包含 4.0, 使用 ASCII(POSIX)排序規(guī)則。
這是一個演示這些問題的腳本:
#!/bin/bash
# test-string: evaluate the value of a string
ANSWER=maybe
if [ -z "$ANSWER" ]; then
echo "There is no answer." >&2
exit 1
fi
if [ "$ANSWER" = "yes" ]; then
echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
echo "The answer is MAYBE."
else
echo "The answer is UNKNOWN."
fi
在這個腳本中,我們計算常量 ANSWER。我們首先確定是否此字符串為空。如果為空,我們就終止 腳本,并把退出狀態(tài)設(shè)為零。注意這個應(yīng)用于 echo 命令的重定向操作。其把錯誤信息 “There is no answer.” 重定向到標(biāo)準(zhǔn)錯誤,這是處理錯誤信息的“合理”方法。如果字符串不為空,我們就計算 字符串的值,看看它是否等于“yes,” "no," 或者“maybe”。為此使用了 elif,它是 “else if” 的簡寫。 通過使用 elif,我們能夠構(gòu)建更復(fù)雜的邏輯測試。
下面的表達(dá)式用于整數(shù):
表達(dá)式 | 如果為真... |
---|---|
integer1 -eq integer2 | integer1 等于 integer2. |
integer1 -ne integer2 | integer1 不等于 integer2. |
integer1 -le integer2 | integer1 小于或等于 integer2. |
integer1 -lt integer2 | integer1 小于 integer2. |
integer1 -ge integer2 | integer1 大于或等于 integer2. |
integer1 -gt integer2 | integer1 大于 integer2. |
這里是一個演示以上表達(dá)式用法的腳本:
#!/bin/bash
# test-integer: evaluate the value of an integer.
INT=-5
if [ -z "$INT" ]; then
echo "INT is empty." >&2
exit 1
fi
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
這個腳本中有趣的地方是怎樣來確定一個整數(shù)是偶數(shù)還是奇數(shù)。通過用模數(shù)2對數(shù)字執(zhí)行求模操作, 就是用數(shù)字來除以2,并返回余數(shù),從而知道數(shù)字是偶數(shù)還是奇數(shù)。
目前的 bash 版本包括一個復(fù)合命令,作為加強(qiáng)的 test 命令替代物。它使用以下語法:
[[ expression ]]
這里,類似于 test,expression 是一個表達(dá)式,其計算結(jié)果為真或假。這個[[ ]]
命令非常
相似于 test 命令(它支持所有的表達(dá)式),但是增加了一個重要的新的字符串表達(dá)式:
string1 =~ regex
其返回值為真,如果 string1匹配擴(kuò)展的正則表達(dá)式 regex。這就為執(zhí)行比如數(shù)據(jù)驗證等任務(wù)提供了許多可能性。
在我們前面的整數(shù)表達(dá)式示例中,如果常量 INT 包含除了整數(shù)之外的任何數(shù)據(jù),腳本就會運(yùn)行失敗。這個腳本
需要一種方法來證明此常量包含一個整數(shù)。使用 [[ ]]
和 =~
字符串表達(dá)式操作符,我們能夠這樣來改進(jìn)腳本:
#!/bin/bash
# test-integer2: evaluate the value of an integer.
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
else
echo "INT is not an integer." >&2
exit 1
fi
通過應(yīng)用正則表達(dá)式,我們能夠限制 INT 的值只是字符串,其開始于一個可選的減號,隨后是一個或多個數(shù)字。 這個表達(dá)式也消除了空值的可能性。
[[ ]]
添加的另一個功能是==
操作符支持類型匹配,正如路徑名展開所做的那樣。例如:
[me@linuxbox ~]$ FILE=foo.bar
[me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then
> echo "$FILE matches pattern 'foo.*'"
> fi
foo.bar matches pattern 'foo.*'
這就使[[ ]]
有助于計算文件和路徑名。
除了 [[ ]]
復(fù)合命令之外,bash 也提供了 (( ))
復(fù)合命名,其有利于操作整數(shù)。它支持一套
完整的算術(shù)計算,我們將在第35章中討論這個主題。
(( ))
被用來執(zhí)行算術(shù)真測試。如果算術(shù)計算的結(jié)果是非零值,則一個算術(shù)真測試值為真。
[me@linuxbox ~]$ if ((1)); then echo "It is true."; fi
It is true.
[me@linuxbox ~]$ if ((0)); then echo "It is true."; fi
[me@linuxbox ~]$
使用(( ))
,我們能夠略微簡化 test-integer2腳本,像這樣:
#!/bin/bash
# test-integer2a: evaluate the value of an integer.
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if ((INT == 0)); then
echo "INT is zero."
else
if ((INT < 0)); then
echo "INT is negative."
else
echo "INT is positive."
fi
if (( ((INT % 2)) == 0)); then
echo "INT is even."
else
echo "INT is odd."
fi
fi
else
echo "INT is not an integer." >&2
exit 1
fi
注意我們使用小于和大于符號,以及==用來測試是否相等。這是使用整數(shù)較為自然的語法了。也要
注意,因為復(fù)合命令 (( ))
是 shell 語法的一部分,而不是一個普通的命令,而且它只處理整數(shù),
所以它能夠通過名字識別出變量,而不需要執(zhí)行展開操作。我們將在第35中進(jìn)一步討論 (( ))
命令
和相關(guān)的算術(shù)展開操作。
也有可能把表達(dá)式結(jié)合起來創(chuàng)建更復(fù)雜的計算。通過使用邏輯操作符來結(jié)合表達(dá)式。我們
在第18章中已經(jīng)知道了這些,當(dāng)我們學(xué)習(xí) find 命令的時候。它們是用于 test 和 [[ ]]
三個邏輯操作。
它們是 AND,OR,和 NOT。test 和 [[ ]]
使用不同的操作符來表示這些操作:
操作符 | 測試 | [[ ]] and (( )) |
---|---|---|
AND | -a | && |
OR | -o | || |
NOT | ! | ! |
這里有一個 AND 操作的示例。下面的腳本決定了一個整數(shù)是否屬于某個范圍內(nèi)的值:
#!/bin/bash
# test-integer3: determine if an integer is within a
# specified range of values.
MIN_VAL=1
MAX_VAL=100
INT=50
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [[ INT -ge MIN_VAL && INT -le MAX_VAL ]]; then
echo "$INT is within $MIN_VAL to $MAX_VAL."
else
echo "$INT is out of range."
fi
else
echo "INT is not an integer." >&2
exit 1
fi
我們也可以對表達(dá)式使用圓括號,為的是分組。如果不使用括號,那么否定只應(yīng)用于第一個 表達(dá)式,而不是兩個組合的表達(dá)式。用 test 可以這樣來編碼:
if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
echo "$INT is in range."
fi
因為 test 使用的所有的表達(dá)式和操作符都被 shell 看作是命令參數(shù)(不像 [[ ]]
和 (( ))
),
對于 bash 有特殊含義的字符,比如說 <,>,(,和 ),必須引起來或者是轉(zhuǎn)義。
知道了 test 和 [[ ]]
基本上完成相同的事情,哪一個更好呢?test 更傳統(tǒng)(是 POSIX 的一部分),
然而 [[ ]]
特定于 bash。知道怎樣使用 test 很重要,因為它被非常廣泛地應(yīng)用,但是顯然 [[ ]]
更
有助于,并更易于編碼。
可移植性是頭腦狹隘人士的心魔
如果你和“真正的”Unix 用戶交談,你很快就會發(fā)現(xiàn)他們大多數(shù)人不是非常喜歡 Linux。他們 認(rèn)為 Linux 骯臟且不干凈。Unix 追隨者的一個宗旨是,一切都應(yīng)“可移植的”。這意味著你編寫 的任意一個腳本都應(yīng)當(dāng)無需修改,就能運(yùn)行在任何一個類 Unix 的系統(tǒng)中。
Unix 用戶有充分的理由相信這一點。在 POSIX 之前,Unix 用戶已經(jīng)看到了命令的專有擴(kuò)展以及 shell 對 Unix 世界的所做所為,他們自然會警惕 Linux 對他們心愛系統(tǒng)的影響。
但是可移植性有一個嚴(yán)重的缺點。它防礙了進(jìn)步。它要求做事情要遵循“最低常見標(biāo)準(zhǔn)”。 在 shell 編程這種情況下,它意味著一切要與 sh 兼容,最初的 Bourne shell。
這個缺點是一個借口,專有軟件供應(yīng)商用它來證明他們的專利擴(kuò)展,只有他們稱他們?yōu)椤皠?chuàng)新”。 但是他們只是為他們的客戶鎖定設(shè)備。
GNU 工具,比如說 bash,就沒有這些限制。他們通過支持標(biāo)準(zhǔn)和普遍地可用性來鼓勵可移植性。你幾乎可以 在所有類型的系統(tǒng)中安裝 bash 和其它的 GNU 工具,甚至是 Windows,而沒有損失。所以就 感覺可以自由的使用 bash 的所有功能。它是真正的可移植。
bash 支持兩種可以執(zhí)行分支任務(wù)的控制操作符。這個 &&(AND)
和||(OR)
操作符作用如同
復(fù)合命令[[ ]]
中的邏輯操作符。這是語法:
command1 && command2
和
command1 || command2
理解這些操作很重要。對于 && 操作符,先執(zhí)行 command1,如果并且只有如果 command1 執(zhí)行成功后, 才會執(zhí)行 command2。對于 || 操作符,先執(zhí)行 command1,如果并且只有如果 command1 執(zhí)行失敗后, 才會執(zhí)行 command2。
在實際中,它意味著我們可以做這樣的事情:
[me@linuxbox ~]$ mkdir temp && cd temp
這會創(chuàng)建一個名為 temp 的目錄,并且若它執(zhí)行成功后,當(dāng)前目錄會更改為 temp。第二個命令會嘗試 執(zhí)行只有當(dāng) mkdir 命令執(zhí)行成功之后。同樣地,一個像這樣的命令:
[me@linuxbox ~]$ [ -d temp ] || mkdir temp
會測試目錄 temp 是否存在,并且只有測試失敗之后,才會創(chuàng)建這個目錄。這種構(gòu)造類型非常有助于在 腳本中處理錯誤,這個主題我們將會在隨后的章節(jié)中討論更多。例如,我們在腳本中可以這樣做:
[ -d temp ] || exit 1
如果這個腳本要求目錄 temp,且目錄不存在,然后腳本會終止,并返回退出狀態(tài)1。
這一章開始于一個問題。我們怎樣使 sys_info_page
腳本來檢測是否用戶擁有權(quán)限來讀取所有的
家目錄?根據(jù)我們的 if 知識,我們可以解決這個問題,通過把這些代碼添加到 report_home_space
函數(shù)中:
report_home_space () {
if [[ $(id -u) -eq 0 ]]; then
cat <<- _EOF_
<H2>Home Space Utilization (All Users)</H2>
<PRE>$(du -sh /home/*)</PRE>
_EOF_
else
cat <<- _EOF_
<H2>Home Space Utilization ($USER)</H2>
<PRE>$(du -sh $HOME)</PRE>
_EOF_
fi
return
}
我們計算 id 命令的輸出結(jié)果。通過帶有 -u 選項的 id 命令,輸出有效用戶的數(shù)字用戶 ID 號。 超級用戶總是零,其它每個用戶是一個大于零的數(shù)字。知道了這點,我們能夠構(gòu)建兩種不同的 here 文檔, 一個利用超級用戶權(quán)限,另一個限制于用戶擁有的家目錄。
我們將暫別 sys_info_page
程序,但不要著急。它還會回來。同時,當(dāng)我們繼續(xù)工作的時候,
將會討論一些我們需要的話題。
bash 手冊頁中有幾部分對本章中涵蓋的主題提供了更詳細(xì)的內(nèi)容:
Lists ( 討論控制操作符 ||
和 &&
)
Compound Commands ( 討論 [[ ]]
, (( ))
和 if )
CONDITIONAL EXPRESSIONS (條件表達(dá)式)
進(jìn)一步,Wikipedia 中有一篇關(guān)于偽代碼概念的好文章: