跳轉到內容

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 中,這也適用於算術表示式(我們將在後面看到)。但在命令級別,情況恰恰相反:退出狀態為零表示“成功”或“真”,而非零退出狀態表示“失敗”或“假”。


注意

確保在 [[]] 前後包含空格,以便 Bash 將其識別為單獨的單詞。類似於 if[[[[-e 之類的用法將無法正常工作。

if 語句

[編輯 | 編輯原始碼]

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.txtdestination.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.txtsource2.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 設定為 TRUEFALSE,我們可以將 $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放在ifelif行的末尾,所以我們在這裡也遵循了這一約定。
  • thenelse之後使用換行符。這些換行符是可選的——它們不需要(也不能)用分號替換——但它們透過視覺上突出顯示if語句的結構來提高可讀性。
  • 常規命令之間用換行符分隔,而不是用分號。這是一個通用的約定,並不特定於if語句。將每個命令放在自己的行上,可以讓使用者更容易地“瀏覽”指令碼並大致瞭解其作用。

這些確切的約定並不特別重要,但遵循一致且易讀的程式碼格式約定是好的。當其他程式設計師檢視您的程式碼時——或者您在寫完程式碼兩個月後檢視您的程式碼時——不一致或不合理的格式會導致難以理解程式碼的含義。

    華夏公益教科書