鍍金池/ 教程/ Linux/ 流程控制:if 分支結(jié)構(gòu)
網(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 眼中看世界
正則表達式

流程控制:if 分支結(jié)構(gòu)

在上一章中,我們遇到一個問題。怎樣使我們的報告生成器腳本能適應(yīng)運行此腳本的用戶的權(quán)限? 這個問題的解決方案要求我們能找到一種方法,在腳本中基于測試條件結(jié)果,來“改變方向”。 用編程術(shù)語表達,就是我們需要程序可以分支。讓我們考慮一個簡單的用偽碼表示的邏輯實例, 偽碼是一種模擬的計算機語言,為的是便于人們理解:

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.”

if

使用 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,從而導致輸出字符串“equals 5”, 第二次是,把 x 的值設(shè)置為0,從而導致輸出字符串“does not equal 5”。

這個 if 語句語法如下:

if commands; then
     commands
[elif commands; then
     commands...]
[else
     commands]
fi

這里的 commands 是指一系列命令。第一眼看到會有點兒困惑。但是在我們弄清楚這些語句之前,我們 必須看一下 shell 是如何評判一個命令的成功與失敗的。

退出狀態(tài)

當命令執(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,表明這個命令遇到了一個錯誤。有些命令使用不同的退出值,來診斷錯誤,而許多命令當 它們執(zhí)行失敗的時候,會簡單地退出并發(fā)送一個數(shù)字1。手冊頁中經(jīng)常會包含一章標題為“退出狀態(tài)”的內(nèi)容, 描述了使用的代碼。然而,一個零總是表明成功。

這個 shell 提供了兩個極其簡單的內(nèi)部命令,它們不做任何事情,除了以一個零或1退出狀態(tài)來終止執(zhí)行。 True 命令總是執(zhí)行成功,而 false 命令總是執(zhí)行失?。?/p>

[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 ~]$

當 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 是一個表達式,其執(zhí)行結(jié)果是 true 或者是 false。當表達式為真時,這個 test 命令返回一個零 退出狀態(tài),當表達式為假時,test 命令退出狀態(tài)為1。

文件表達式

以下表達式被用來計算文件狀態(tài):

表28-1: 測試文件表達式
表達式 如果為真
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 是一個定向到終端/從終端定向的文件描述符 。 這可以被用來決定是否重定向了標準輸入/輸出錯誤。
-u file file 存在并且設(shè)置了 setuid 位。
-w file file 存在并且可寫(有效用戶擁有可寫權(quán)限)。
-x file file 存在并且可執(zhí)行(有效用戶有執(zhí)行/搜索權(quán)限)。

這里我們有一個腳本說明了一些文件表達式:

#!/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é)果。對于此腳本有兩點需要注意。第一個, 在表達式中參數(shù)$FILE是怎樣被引用的。引號并不是必需的,但這是為了防范空參數(shù)。如果$FILE的參數(shù)展開 是一個空值,就會導致一個錯誤(操作符將會被解釋為非空的字符串而不是操作符)。用引號把參數(shù)引起來就 確保了操作符之后總是跟隨著一個字符串,即使字符串為空。第二個,注意腳本末尾的 exit 命令。 這個 exit 命令接受一個單獨的,可選的參數(shù),其成為腳本的退出狀態(tài)。當不傳遞參數(shù)時,退出狀態(tài)默認為零。 以這種方式使用 exit 命令,則允許此腳本提示失敗如果 $FILE 展開成一個不存在的文件名。這個 exit 命令 出現(xià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
}

字符串表達式

以下表達式用來計算字符串:

表28-2: 測試字符串表達式
表達式 如果為真...
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 之前。

警告:這個 > 和 <表達式操作符必須用引號引起來(或者是用反斜杠轉(zhuǎn)義), 當與 test 一塊使用的時候。如果不這樣,它們會被 shell 解釋為重定向操作符,造成潛在地破壞結(jié)果。 同時也要注意雖然 bash 文檔聲明排序遵從當前語系的排列規(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.” 重定向到標準錯誤,這是處理錯誤信息的“合理”方法。如果字符串不為空,我們就計算 字符串的值,看看它是否等于“yes,” "no," 或者“maybe”。為此使用了 elif,它是 “else if” 的簡寫。 通過使用 elif,我們能夠構(gòu)建更復雜的邏輯測試。

整型表達式

下面的表達式用于整數(shù):

表28-3: 測試整數(shù)表達式
表達式 如果為真...
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.

這里是一個演示以上表達式用法的腳本:

#!/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ù)。

更現(xiàn)代的測試版本

目前的 bash 版本包括一個復合命令,作為加強的 test 命令替代物。它使用以下語法:

[[ expression ]]

這里,類似于 test,expression 是一個表達式,其計算結(jié)果為真或假。這個[[ ]]命令非常 相似于 test 命令(它支持所有的表達式),但是增加了一個重要的新的字符串表達式:

string1 =~ regex

其返回值為真,如果 string1匹配擴展的正則表達式 regex。這就為執(zhí)行比如數(shù)據(jù)驗證等任務(wù)提供了許多可能性。 在我們前面的整數(shù)表達式示例中,如果常量 INT 包含除了整數(shù)之外的任何數(shù)據(jù),腳本就會運行失敗。這個腳本 需要一種方法來證明此常量包含一個整數(shù)。使用 [[ ]]=~ 字符串表達式操作符,我們能夠這樣來改進腳本:

#!/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)用正則表達式,我們能夠限制 INT 的值只是字符串,其開始于一個可選的減號,隨后是一個或多個數(shù)字。 這個表達式也消除了空值的可能性。

[[ ]]添加的另一個功能是==操作符支持類型匹配,正如路徑名展開所做的那樣。例如:

[me@linuxbox ~]$ FILE=foo.bar
[me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then
> echo "$FILE matches pattern 'foo.*'"
> fi
foo.bar matches pattern 'foo.*'

這就使[[ ]]有助于計算文件和路徑名。

(( )) - 為整數(shù)設(shè)計

除了 [[ ]] 復合命令之外,bash 也提供了 (( )) 復合命名,其有利于操作整數(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ù)較為自然的語法了。也要 注意,因為復合命令 (( )) 是 shell 語法的一部分,而不是一個普通的命令,而且它只處理整數(shù), 所以它能夠通過名字識別出變量,而不需要執(zhí)行展開操作。我們將在第35中進一步討論 (( )) 命令 和相關(guān)的算術(shù)展開操作。

結(jié)合表達式

也有可能把表達式結(jié)合起來創(chuàng)建更復雜的計算。通過使用邏輯操作符來結(jié)合表達式。我們 在第18章中已經(jīng)知道了這些,當我們學習 find 命令的時候。它們是用于 test 和 [[ ]] 三個邏輯操作。 它們是 AND,OR,和 NOT。test 和 [[ ]] 使用不同的操作符來表示這些操作:

表28-4: 邏輯操作符
操作符 測試 [[ ]] 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

我們也可以對表達式使用圓括號,為的是分組。如果不使用括號,那么否定只應(yīng)用于第一個 表達式,而不是兩個組合的表達式。用 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 使用的所有的表達式和操作符都被 shell 看作是命令參數(shù)(不像 [[ ]](( )) ), 對于 bash 有特殊含義的字符,比如說 <,>,(,和 ),必須引起來或者是轉(zhuǎn)義。

知道了 test 和 [[ ]] 基本上完成相同的事情,哪一個更好呢?test 更傳統(tǒng)(是 POSIX 的一部分), 然而 [[ ]] 特定于 bash。知道怎樣使用 test 很重要,因為它被非常廣泛地應(yīng)用,但是顯然 [[ ]] 更 有助于,并更易于編碼。

可移植性是頭腦狹隘人士的心魔

如果你和“真正的”Unix 用戶交談,你很快就會發(fā)現(xiàn)他們大多數(shù)人不是非常喜歡 Linux。他們 認為 Linux 骯臟且不干凈。Unix 追隨者的一個宗旨是,一切都應(yīng)“可移植的”。這意味著你編寫 的任意一個腳本都應(yīng)當無需修改,就能運行在任何一個類 Unix 的系統(tǒng)中。

Unix 用戶有充分的理由相信這一點。在 POSIX 之前,Unix 用戶已經(jīng)看到了命令的專有擴展以及 shell 對 Unix 世界的所做所為,他們自然會警惕 Linux 對他們心愛系統(tǒng)的影響。

但是可移植性有一個嚴重的缺點。它防礙了進步。它要求做事情要遵循“最低常見標準”。 在 shell 編程這種情況下,它意味著一切要與 sh 兼容,最初的 Bourne shell。

這個缺點是一個借口,專有軟件供應(yīng)商用它來證明他們的專利擴展,只有他們稱他們?yōu)椤皠?chuàng)新”。 但是他們只是為他們的客戶鎖定設(shè)備。

GNU 工具,比如說 bash,就沒有這些限制。他們通過支持標準和普遍地可用性來鼓勵可移植性。你幾乎可以 在所有類型的系統(tǒng)中安裝 bash 和其它的 GNU 工具,甚至是 Windows,而沒有損失。所以就 感覺可以自由的使用 bash 的所有功能。它是真正的可移植。

控制操作符:分支的另一種方法

bash 支持兩種可以執(zhí)行分支任務(wù)的控制操作符。這個 &&(AND)||(OR)操作符作用如同 復合命令[[ ]]中的邏輯操作符。這是語法:

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í)行成功后,當前目錄會更改為 temp。第二個命令會嘗試 執(zhí)行只有當 mkdir 命令執(zhí)行成功之后。同樣地,一個像這樣的命令:

[me@linuxbox ~]$ [ -d temp ] || mkdir temp

會測試目錄 temp 是否存在,并且只有測試失敗之后,才會創(chuàng)建這個目錄。這種構(gòu)造類型非常有助于在 腳本中處理錯誤,這個主題我們將會在隨后的章節(jié)中討論更多。例如,我們在腳本中可以這樣做:

[ -d temp ] || exit 1

如果這個腳本要求目錄 temp,且目錄不存在,然后腳本會終止,并返回退出狀態(tài)1。

總結(jié)

這一章開始于一個問題。我們怎樣使 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 程序,但不要著急。它還會回來。同時,當我們繼續(xù)工作的時候, 將會討論一些我們需要的話題。

拓展閱讀

bash 手冊頁中有幾部分對本章中涵蓋的主題提供了更詳細的內(nèi)容:

  • Lists ( 討論控制操作符 ||&& )

  • Compound Commands ( 討論 [[ ]], (( )) 和 if )

  • CONDITIONAL EXPRESSIONS (條件表達式)

  • SHELL BUILTIN COMMANDS ( 討論 test )

進一步,Wikipedia 中有一篇關(guān)于偽代碼概念的好文章:

http://en.wikipedia.org/wiki/Pseudocode