Bash Shell 指令碼/條件表示式
很多時候,我們希望只有在特定條件滿足時才執行某個命令。例如,我們可能希望僅當 source.txt 存在時執行命令 cp source.txt destination.txt(“將檔案 source.txt 複製到位置 destination.txt”)。我們可以像這樣做到
#!/bin/bash
if [[ -e source.txt ]] ; then
cp source.txt destination.txt
fi
以上使用了兩個內建命令
- 構造
[[ condition ]]如果condition為真,則返回退出狀態為零(成功),如果condition為假,則返回非零退出狀態(失敗)。在我們的例子中,condition是-e source.txt,如果且僅當存在名為source.txt的檔案時,此條件為真。 - 構造
if command1 ; then
command2
fi
command1;如果成功完成(即退出狀態為零),則繼續執行command2。
換句話說,上面等價於
#!/bin/bash
[[ -e source.txt ]] && cp source.txt destination.txt
只是它更清晰(並且更靈活,在我們將很快看到的方式中)。
一般來說,Bash 將成功的退出狀態(零)視為“真”,將失敗的退出狀態(非零)視為“假”,反之亦然。例如,內建命令 true 始終“成功”(返回零),而內建命令 false 始終“失敗”(返回一)。
| 注意 在許多常用的程式語言中,零被認為是“假”,非零值被認為是“真”。即使在 Bash 中,這也適用於算術表示式(我們將在後面看到)。但在命令級別,情況恰恰相反:退出狀態為零表示“成功”或“真”,而非零退出狀態表示“失敗”或“假”。 |
| 注意 確保在 |
if 語句比我們上面看到的更靈活;我們實際上可以指定多個 命令在測試命令成功時執行,此外,我們還可以使用 else 子句來指定一個或多個命令在測試命令失敗時執行
#!/bin/bash
if [[ -e source.txt ]] ; then
echo 'source.txt exists; copying to destination.txt.'
cp source.txt destination.txt
else
echo 'source.txt does not exist; exiting.'
exit 1 # terminate the script with a nonzero exit status (failure)
fi
這些命令甚至可以包含其他 if 語句;也就是說,一個 if 語句可以“巢狀”在另一個 if 語句中。在這個例子中,一個 if 語句巢狀在另一個 if 語句的 else 子句中
#!/bin/bash
if [[ -e source1.txt ]] ; then
echo 'source1.txt exists; copying to destination.txt.'
cp source1.txt destination.txt
else
if [[ -e source2.txt ]] ; then
echo 'source1.txt does not exist, but source2.txt does.'
echo 'Copying source2.txt to destination.txt.'
cp source2.txt destination.txt
else
echo 'Neither source1.txt nor source2.txt exists; exiting.'
exit 1 # terminate the script with a nonzero exit status (failure)
fi
fi
這種特殊的模式——一個 else 子句,它只包含一個 if 語句,代表一個回退測試——非常常見,以至於 Bash 為它提供了一個方便的簡寫符號,使用 elif(“else-if”)子句。上面的例子可以這樣寫
#!/bin/bash
if [[ -e source1.txt ]] ; then
echo 'source1.txt exists; copying to destination.txt.'
cp source1.txt destination.txt
elif [[ -e source2.txt ]] ; then
echo 'source1.txt does not exist, but source2.txt does.'
echo 'Copying source2.txt to destination.txt.'
cp source2.txt destination.txt
else
echo 'Neither source1.txt nor source2.txt exists; exiting.'
exit 1 # terminate the script with a nonzero exit status (failure)
fi
單個 if 語句可以有任意數量的 elif 子句,代表任意數量的回退條件。
最後,有時我們希望在條件為假時執行一個命令,而沒有相應的命令在條件為真時執行。為此,我們可以使用內建的 ! 運算子,它位於命令之前;當命令返回零(成功或“真”)時,! 運算子會更改返回一個非零值(失敗或“假”),反之亦然。例如,以下語句將複製 source.txt 到 destination.txt,除非 destination.txt 已經存在
#!/bin/bash
if ! [[ -e destination.txt ]] ; then
cp source.txt destination.txt
fi
以上所有示例都是使用 test 表示式的示例。實際上,if 只會在語句中的命令返回 0 時執行 then 中的所有內容
# First build a function that simply returns the code given
returns() { return $*; }
# Then use read to prompt user to try it out, read `help read' if you have forgotten this.
read -p "Exit code:" exit
if (returns $exit)
then echo "true, $?"
else echo "false, $?"
fi
因此,if 的行為在某些方面類似於邏輯“與”&& 和“或”||
# Let's reuse the returns function.
returns() { return $*; }
read -p "Exit code:" exit
# if ( and ) else fi
returns $exit && echo "true, $?" || echo "false, $?"
# The REAL equivalent, false is like `returns 1'
# Of course you can use the returns $exit instead of false.
# (returns $exit ||(echo "false, $?"; false)) && echo "true, $?"
始終注意,誤用這些邏輯運算子可能會導致錯誤。在上面的例子中,一切正常,因為普通的 echo 幾乎總是成功的。
除了上面使用的 -e file 條件(如果 file 存在則為真)之外,Bash 的 [[ … ]] 符號還支援相當多的條件型別。五個最常用的條件是
-d file- 如果
file存在且為目錄,則為真。 -f file- 如果
file存在且為普通檔案,則為真。 -e file- 如果
file存在,無論其是什麼,都為真。 string == pattern- 如果
string匹配pattern,則為真。(pattern的形式與檔名擴充套件中的模式相同;例如,未引用的*表示“零個或多個字元”。) string != pattern- 如果
string不 匹配pattern,則為真。 string =~ regexp- 如果
string包含 Posix 擴充套件正則表示式regexp,則為真。有關更多資訊,請參閱 正則表示式/POSIX 擴充套件正則表示式。
在最後三種類型的測試中,左側的值通常是變數擴充套件;例如,[[ "$var" = 'value' ]] 如果名為 var 的變數包含值 value,則返回成功的退出狀態。
以上條件只是觸及了表面;還有許多其他條件可以檢查檔案,一些其他條件可以檢查字串,幾個條件可以檢查整數值,以及一些其他不屬於這些組的條件。
平等測試的一個常見用途是檢視指令碼的第一個引數($1)是否是一個特殊選項。例如,考慮我們上面的 if 語句,它試圖將 source1.txt 或 source2.txt 複製到 destination.txt。上面的版本非常“冗長”:它產生了大量的輸出。通常我們不希望指令碼生成太多輸出;但我們可能希望使用者能夠請求輸出,例如透過將 --verbose 作為第一個引數傳遞。以下指令碼等價於上面的 if 語句,但它只在第一個引數是 --verbose 時列印輸出
#!/bin/bash
if [[ "$1" == --verbose ]] ; then
verbose_mode=TRUE
shift # remove the option from $@
else
verbose_mode=FALSE
fi
if [[ -e source1.txt ]] ; then
if [[ "$verbose_mode" == TRUE ]] ; then
echo 'source1.txt exists; copying to destination.txt.'
fi
cp source1.txt destination.txt
elif [[ -e source2.txt ]] ; then
if [[ "$verbose_mode" == TRUE ]] ; then
echo 'source1.txt does not exist, but source2.txt does.'
echo 'Copying source2.txt to destination.txt.'
fi
cp source2.txt destination.txt
else
if [[ "$verbose_mode" == TRUE ]] ; then
echo 'Neither source1.txt nor source2.txt exists; exiting.'
fi
exit 1 # terminate the script with a nonzero exit status (failure)
fi
稍後,當我們學習 shell 函式時,我們將找到一種更緊湊的方式來表達這一點。(事實上,即使我們已經知道,也有一種更緊湊的方式來表達這一點:而不是將 $verbose_mode 設定為 TRUE 或 FALSE,我們可以將 $echo_if_verbose_mode 設定為 echo 或 :,其中冒號 : 是一個什麼也不做的 Bash 內建命令。然後我們可以用 "$echo_if_verbose_mode" 替換所有 echo 的使用。然後,像 "$echo_if_verbose_mode" message 這樣的命令將變成 echo message,列印 message,如果 verbose 模式開啟,否則將變成 : message,什麼也不做,如果 verbose 模式關閉。但是,這種方法可能比真正值得的更令人困惑,因為目的很簡單。)
要將多個條件與“與”或“或”組合,或使用“非”反轉條件,我們可以使用我們已經看到的通用 Bash 符號。考慮這個例子
#!/bin/bash
if [[ -e source.txt ]] && ! [[ -e destination.txt ]] ; then
# source.txt exists, destination.txt does not exist; perform the copy:
cp source.txt destination.txt
fi
測試命令 [[ -e source.txt ]] && ! [[ -e destination.txt ]] 使用了我們上面看到的基於退出狀態的 && 和 ! 運算子。[[ condition ]] 如果 condition 為真,則“成功”,這意味著 [[ -e source.txt ]] && ! [[ -e destination.txt ]] 只有在 source.txt 存在時才會執行 ! [[ -e destination.txt ]]。此外,! 反轉了 [[ -e destination.txt ]] 的退出狀態,因此 ! [[ -e destination.txt ]] 如果且僅當 destination.txt 不存在 時才“成功”。最終結果是 [[ -e source.txt ]] && ! [[ -e destination.txt ]] 如果且僅當 source.txt 存在 且 destination.txt 不存在 時才“成功”——“真”。
構造 [[ … ]] 實際上對這些運算子有內建的內部支援,這樣我們也可以這樣寫上面程式碼
#!/bin/bash
if [[ -e source.txt && ! -e destination.txt ]] ; then
# source.txt exists, destination.txt does not exist; perform the copy:
cp source.txt destination.txt
fi
但是,通用符號通常更清晰;當然,它們可以與任何測試命令一起使用,而不僅僅是 [[ … ]] 構造。
上面示例中的if語句經過格式化,以便人類易於閱讀和理解。這不僅對書中的示例很重要,對現實世界中的指令碼也很重要。具體而言,上述示例遵循以下約定
if語句中的命令以一致的量縮排(恰好縮排兩個空格)。這種縮排對 Bash 來說無關緊要——它忽略行首的空白——但對人類程式設計師來說非常重要。如果沒有它,就很難看清if語句的開始和結束位置,甚至很難看出有if語句。當if語句巢狀在if語句中(或其他各種型別的控制結構中,我們將在後面看到)時,一致的縮排就變得更加重要。- 分號字元
;用在then之前。這是一個用於分隔命令的特殊運算子;它在大多數情況下等同於換行符,儘管存在一些差異(例如,註釋始終從#執行到行尾,而不是從#執行到;)。我們可以將then放在新行的開頭,這完全沒問題,但對單個指令碼來說,保持一致的風格比較好;對普通結構使用單一一致的外觀,可以更容易地注意到不尋常的結構。在現實世界中,程式設計師通常將; then放在if或elif行的末尾,所以我們在這裡也遵循了這一約定。 - 在
then和else之後使用換行符。這些換行符是可選的——它們不需要(也不能)用分號替換——但它們透過視覺上突出顯示if語句的結構來提高可讀性。 - 常規命令之間用換行符分隔,而不是用分號。這是一個通用的約定,並不特定於
if語句。將每個命令放在自己的行上,可以讓使用者更容易地“瀏覽”指令碼並大致瞭解其作用。
這些確切的約定並不特別重要,但遵循一致且易讀的程式碼格式約定是好的。當其他程式設計師檢視您的程式碼時——或者您在寫完程式碼兩個月後檢視您的程式碼時——不一致或不合理的格式會導致難以理解程式碼的含義。