您是否為 Bash shell 中大量的測試和比較選項而困惑呢?這個技巧可以幫助您解密不同類型的文件、算術和字符串測試,這樣您就能夠知道什么時候使用test
、[?]
、[[?]]
、((?))
或if-then-else
了。
Bash shell 在當今的許多 Linux? 和 UNIX? 系統上都可使用,是 Linux 上常見的默認 shell。Bash 包含強大的編程功能,其中包括豐富的可測試文件類型和屬性的函數,以及在多數編程語言中可以使用的算術和字符串比較函數。理解不同的測試并認識到 shell 還能把一些操作符解釋成 shell 元字符,是成為高級 shell 用戶的重要一步。這篇文章摘自 developerWorks 教程 LPI exam 102 prep: Shells, scripting, programming, and compiling,介紹了如何理解和使用 Bash shell 的測試和比較操作。
這個技巧解釋了 shell 測試和比較函數,演示了如何向 shell 添加編程功能。您可能已經看到過使用 && 和 || 操作符的簡單 shell 邏輯,它允許您根據前一條命令的退出狀態(正確退出或伴隨錯誤退出)而執行后一條命令。在這個技巧中,將看到如何把這些基本的技術擴展成更復雜的 shell 編程。
測試
在任何一種編程語言中,學習了如何給變量分配值和傳遞參數之后,都需要測試這些值和參數。在 shell 中,測試會設置返回的狀態,這與其他命令執行的功能相同。實際上,test
是個內置命令!
test 和 [
內置命令 test
根據表達式expr 求值的結果返回 0(真)或 1(假)。也可以使用方括號:test?expr
和 [ expr ] 是等價的。 可以用 $?
檢查返回值;可以使用 && 和 || 操作返回值;也可以用本技巧后面介紹的各種條件結構測試返回值。
清單 1. 一些簡單測試
[ian@pinguino ~]$ test 3 -gt 4 && echo True || echo false false [ian@pinguino ~]$ [ "abc" != "def" ];echo $? 0 [ian@pinguino ~]$ test -d "$HOME" ;echo $? 0 |
在清單 1 的第一個示例中,-gt
操作符對兩個字符值之間執行算術比較。在第二個示例中,用 [?]
的形式比較兩個字符串不相等。在最后一個示例中,測試 HOME 變量的值,用單目操作符 -d
檢查它是不是目錄。
可以用 -eq
、 -ne
、-lt
、 -le
、 -gt
或 -ge
比較算術值,它們分別表示等于、不等于、小于、小于等于、大于、大于等于。
可以分別用操作符 =
、 !=
、<
和 >
比較字符串是否相等、不相等或者第一個字符串的排序在第二個字符串的前面或后面。單目操作符 -z
測試 null 字符串,如果字符串非空 -n
返回 True(或者根本沒有操作符)。
說明:shell 也用 <
和 >
操作符進行重定向,所以必須用 \<
或 \>
加以轉義。清單 2 顯示了字符串測試的更多示例。檢查它們是否如您預期的一樣。
清單 2. 一些字符串測試
[ian@pinguino ~]$ test "abc" = "def" ;echo $? 1 [ian@pinguino ~]$ [ "abc" != "def" ];echo $? 0 [ian@pinguino ~]$ [ "abc" \< "def" ];echo $? 0 [ian@pinguino ~]$ [ "abc" \> "def" ];echo $? 1 [ian@pinguino ~]$ [ "abc" \<"abc" ];echo $? 1 [ian@pinguino ~]$ [ "abc" \> "abc" ];echo $? 1 |
表 1 顯示了一些更常見的文件測試。如果被測試的文件存在,而且有指定的特征,則結果為 True。
操作符 | 特征 |
---|---|
-d | 目錄 |
-e | 存在(也可以用 -a) |
-f | 普通文件 |
-h | 符號連接(也可以用 -L) |
-p | 命名管道 |
-r | 可讀 |
-s | 非空 |
-S | 套接字 |
-w | 可寫 |
-N | 從上次讀取之后已經做過修改 |
除了上面的單目測試,還可以使用表 2 所示的雙目操作符比較兩個文件:
操作符 | 為 True 的情況 |
---|---|
-nt | 測試 file1 是否比 file2 更新。修改日期將用于這次和下次比較。 |
-ot | 測試 file1 是否比 file2 舊。 |
-ef | 測試 file1 是不是 file2 的硬鏈接。 |
其他一些測試可以用來測試文件許可之類的內容。請參閱 bash 手冊獲得更多細節或使用 help?test
查看內置測試的簡要信息。也可以用 help
命令了解其他內置命令。
-o
操作符允許測試利用 set?-o?選項
設置的各種 shell 選項,如果設置了該選項,則返回 True (0),否則返回 False (1),如清單 3 所示。
清單 3. 測試 shell 選項
[ian@pinguino ~]$ set +o nounset [ian@pinguino ~]$ [ -o nounset ];echo $? 1 [ian@pinguino ~]$ set -u [ian@pinguino ~]$ test -o nounset; echo $? 0 |
最后,-a
和 -o
選項允許使用邏輯運算符 AND 和 OR 將表達式組合在一起。單目操作符 !
可以使測試的意義相反??梢杂美ㄌ柊驯磉_式分組,覆蓋默認的優先級。請記住 shell 通常要在子 shell 中運行括號中的表達式,所以需要用 \( 和 \) 轉義括號,或者把這些操作符括在單引號或雙引號內。清單 4 演示了摩根法則在表達式上的應用。
清單 4. 組合和分組測試
[ian@pinguino ~]$ test "a" != "$HOME" -a 3 -ge 4 ; echo $? 1 [ian@pinguino ~]$ [ ! \( "a" = "$HOME" -o 3 -lt 4 \) ]; echo $? 1 [ian@pinguino ~]$ [ ! \( "a" = "$HOME" -o '(' 3 -lt 4 ')' ")" ]; echo $? 1 |
(( 和 [[
test
命令非常強大,但是很難滿足其轉義需求以及字符串和算術比較之間的區別。幸運的是,bash 提供了其他兩種測試方式,這兩種方式對熟悉 C、C++ 或 Java? 語法的人來說會更自然些。
((?))
復合命令 計算算術表達式,如果表達式求值為 0,則設置退出狀態為 1;如果求值為非 0 值,則設置為 0。不需要對 ((
和 ))
之間的操作符轉義。算術只對整數進行。除 0 會產生錯誤,但不會產生溢出。可以執行 C 語言中常見的算術、邏輯和位操作。 let
命令也能執行一個或多個算術表達式。它通常用來為算術變量分配值。
清單 5. 分配和測試算術表達式
[ian@pinguino ~]$ let x=2 y=2**3 z=y*3;echo $? $x $y $z 0 2 8 24 [ian@pinguino ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w 0 3 8 16 [ian@pinguino ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w 0 4 8 13 |
同使用 ((?))
一樣,利用復合命令 [[?]]
可以對文件名和字符串使用更自然的語法??梢杂美ㄌ柡瓦壿嫴僮鞣?test
命令支持的測試組合起來。
清單 6. 使用 [[ 復合命令
[ian@pinguino ~]$ [[ ( -d "$HOME" ) && ( -w "$HOME" ) ]] && > echo "home is a writable directory" home is a writable directory |
在使用 =
或 !=
操作符時,復合命令 [[
還能在字符串上進行模式匹配。匹配的方式就像清單 7 所示的通配符匹配。
清單 7. 用 [[ 進行通配符測試
[ian@pinguino ~]$ [[ "abc def .d,x--" == a[abc]*\ ?d* ]]; echo $? 0 [ian@pinguino ~]$ [[ "abc def c" == a[abc]*\ ?d* ]]; echo $? 1 [ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* ]]; echo $? 1 |
甚至還可以在 [[
復合命令內執行算術測試,但是千萬要小心。除非在 ((
復合命令內,否則 <
和 >
操作符會把操作數當成字符串比較并在當前排序序列中測試它們的順序。清單 8 用一些示例演示了這一點。
清單 8. 用 [[ 包含算術測試
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || (( 3 > 2 )) ]]; echo $? 0 [ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 -gt 2 ]]; echo $? 0 [ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 > 2 ]]; echo $? 0 [ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a > 2 ]]; echo $? 0 [ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a -gt 2 ]]; echo $? -bash: a: unbound variable |
條件測試
雖然使用以上的測試和 &&
、 ||
控制操作符能實現許多編程,但 bash 還包含了更熟悉的 “if, then, else” 和 case 結構。學習完這些之后,將學習循環結構,這樣您的工具箱將真正得到擴展。
If、then、else 語句
bash 的 if
命令是個復合命令,它測試一個測試或命令($?
)的返回值,并根據返回值為 True(0)或 False(不為 0)進行分支。雖然上面的測試只返回 0 或 1 值,但命令可能返回其他值。請參閱 LPI exam 102 prep: Shells, scripting, programming, and compiling 教程學習這方面的更多內容。
Bash 中的 if
命令有一個 then
子句,子句中包含測試或命令返回 0 時要執行的命令列表,可以有一個或多個可選的 elif
子句,每個子句可執行附加的測試和一個 then
子句,子句中又帶有相關的命令列表,最后是可選的 else
子句及命令列表,在前面的測試或 elif
子句中的所有測試都不為真的時候執行,最后使用 fi
標記表示該結構結束。
使用迄今為止學到的東西,現在能夠構建簡單的計算器來計算算術表達式,如清單 9 所示:
清單 9. 用 if、then、else 計算表達式
[ian@pinguino ~]$ function mycalc () > { > local x > if [ $# -lt 1 ]; then > echo "This function evaluates arithmetic for you if you give it some" > elif (( $* )); then > let x="$*" > echo "$* = $x" > else > echo "$* = 0 or is not an arithmetic expression" > fi > } [ian@pinguino ~]$ mycalc 3 + 4 3 + 4 = 7 [ian@pinguino ~]$ mycalc 3 + 4**3 3 + 4**3 = 67 [ian@pinguino ~]$ mycalc 3 + (4**3 /2) -bash: syntax error near unexpected token `(' [ian@pinguino ~]$ mycalc 3 + "(4**3 /2)" 3 + (4**3 /2) = 35 [ian@pinguino ~]$ mycalc xyz xyz = 0 or is not an arithmetic expression [ian@pinguino ~]$ mycalc xyz + 3 + "(4**3 /2)" + abc xyz + 3 + (4**3 /2) + abc = 35 |
這個計算器利用 local
語句將 x 聲明為局部變量,只能在 mycalc
函數的范圍內使用。let
函數具有幾個可用的選項,可以執行與它密切關聯的 declare
函數。請參考 bash 手冊或使用 help?let
獲得更多信息。
如清單 9 所示,需要確保在表達式使用 shell 元字符 —— 例如(、)、*、> 和 < 時 —— 正確地對表達式轉義。無論如何,現在有了一個非常方便的小計算器,可以像 shell 那樣進行算術計算。
在清單 9 中可能注意到 else
子句和最后的兩個示例??梢钥吹?,把 xyz
傳遞給 mycalc 并沒有錯誤,但計算結果為 0。這個函數還不夠靈巧,不能區分最后使用的示例中的字符值,所以不能警告用戶??梢允褂米址J狡ヅ錅y試(例如 [[ ! ("$*" == *[a-zA-Z]* ]]
,或使用適合自己范圍的形式)消除包含字母表字符的表達式,但是這會妨礙在輸入中使用 16 進制標記,因為使用 16 進制標記時可能要用 0x0f 表示 15。實際上,shell 允許的基數最高為 64(使用 base#value
標記),所以可以在輸入中加入 _ 和 @ 合法地使用任何字母表字符。8 進制和 16 進制使用常用的標記方式,開頭為 0 表示八進制,開頭為 0x 或 0X 表示 16 進制。清單 10 顯示了一些示例。
清單 10. 用不同的基數進行計算
[ian@pinguino ~]$ mycalc 015 015 = 13 [ian@pinguino ~]$ mycalc 0xff 0xff = 255 [ian@pinguino ~]$ mycalc 29#37 29#37 = 94 [ian@pinguino ~]$ mycalc 64#1az 64#1az = 4771 [ian@pinguino ~]$ mycalc 64#1azA 64#1azA = 305380 [ian@pinguino ~]$ mycalc 64#1azA_@ 64#1azA_@ = 1250840574 [ian@pinguino ~]$ mycalc 64#1az*64**3 + 64#A_@ 64#1az*64**3 + 64#A_@ = 1250840574 |
對輸入進行的額外處理超出了本技巧的范圍,所以請小心使用這個計算器。
elif
語句非常方便。它允許簡化縮進,從而有助于腳本編寫。在清單 11 中可能會對 type
命令在 mycalc
函數中的輸出感到驚訝。
清單 11. Type mycalc
[ian@pinguino ~]$ type mycalc mycalc is a function mycalc () { local x; if [ $# -lt 1 ]; then echo "This function evaluates arithmetic for you if you give it some"; else if (( $* )); then let x="$*"; echo "$* = $x"; else echo "$* = 0 or is not an arithmetic expression"; fi; fi } |
當然,也可以只用 $((?表達式?))
和 echo
命令進行 shell 算術運算,如清單 12 所示。這樣就不必學習關于函數或測試的任何內容,但是請注意 shell 不會解釋元字符,例如 *,因此元字符不能在 ((?表達式?))
或 [[?表達式?]]
中那樣正常發揮作用。
清單 12. 在 shell 中用 echo 和 $(( )) 直接進行計算
[ian@pinguino ~]$ echo $((3 + (4**3 /2))) 35 |