Bourne Shell 指令碼/控制流
到目前為止,我們已經討論了基礎知識和理論。我們已經涵蓋了可用的不同 shell 以及如何在 Bourne Shell 中執行 shell 指令碼。我們已經討論了 Unix 環境,並且我們已經看到你擁有控制環境的變數,以及可以用來儲存供自己使用的值的變數。然而,我們還沒有真正做任何事情。我們還沒有讓系統行動,跳過障礙,去取報紙或洗碗。
在本章中,是時候認真對待了。在本章中,我們將討論程式設計 - 如何編寫能夠做出決策並執行命令的程式。在本章中,我們將討論控制流和命令執行。
程式啟動器和命令 shell 之間有什麼區別?為什麼 Bourne Shell 是一款在全球範圍內擁有強大力量和尊重的工具,而不僅僅是一個用來啟動“真正”程式的愚蠢小工具?因為 Bourne Shell 不僅僅是一個啟動程式的環境:Bourne Shell 是一個完全可程式設計的環境,擁有完整程式語言的力量。我們在環境中已經看到 Bourne Shell 在記憶體中擁有變數。但 Bourne Shell 能做的不止這些:它可以做出決策並重覆命令。像任何真正的程式語言一樣,Bourne Shell 擁有控制流,可以控制計算機。
在我們能夠在 shell 指令碼中做出決策之前,我們需要一種評估條件的方法。我們必須能夠檢查某些情況的狀態,以便能夠根據我們的發現做出決策。
奇怪的是,實際的 shell 不包含任何此類機制。有一個專門用於此目的的工具稱為 test(它確實是為 shell 指令碼建立的),但它嚴格來說並不是 shell 的一部分。'test' 工具評估條件並返回 true 或 false,具體取決於它發現了什麼。它以退出狀態的形式返回這些值(在 $? shell 變數中):0 表示 true,其他值表示 false。test 命令的一般形式是
例如
test "Hello World" = "Hello World"
此字串相等性測試返回退出狀態 0。'test' 還有一種簡寫形式,它在指令碼中通常更具可讀性,即方括號
請注意括號和實際條件之間的空格 - 不要忘記在自己的指令碼中新增空格。上面示例的簡寫形式是
[ "Hello World" = "Hello World" ]
'Test' 可以評估許多不同型別的條件,以適應你可能想要在 shell 指令碼中進行的不同型別的測試。大多數特定 shell 都在基本條件集的基礎上進行了擴充套件,但 Bourne Shell 識別以下條件
檔案條件
- -b file
- file 存在且是塊特殊檔案
- -c file
- file 存在且是字元特殊檔案
- -d file
- file 存在且是目錄
- -f file
- file 存在且是常規資料檔案
- -g file
- file 存在且其設定組 ID 位已設定
- -k file
- file 存在且其粘滯位已設定
- -p file
- file 存在且是命名管道
- -r file
- file 存在且可讀
- -s file
- file 存在且其大小大於零
- -t [n]
- 具有編號 n 的開啟檔案描述符是終端裝置;n 是可選的,預設值為 1
- -u file
- file 存在且其設定使用者 ID 位已設定
- -w file
- file 存在且可寫
- -x file
- file 存在且可執行
字串條件
- -n s
- s 長度非零
- -z s
- s 長度為零
- s0 = s1
- s0 和 s1 相同
- s0 != s1
- s0 和 s1 不同
- s
- s 不為空(通常用於檢查環境變數是否有值)
整數條件
- n0 -eq n1
- n0 等於 n1
- n0 -ge n1
- n0 大於或等於 n1
- n0 -gt n1
- n0 嚴格大於 n1
- n0 -le n1
- n0 小於或等於 n1
- n0 -lt n1
- n0 嚴格小於 n1
- n0 -ne n1
- n0 不等於 n1
最後,條件可以組合和分組
- \( B \)
- 括號用於分組條件(不要忘記反斜槓)。如果 B 為真,則分組條件 (B) 為真。
- ! B
- 否定;如果 B 為假,則為真。
- B0 -a B1
- 並且;如果 B0 和 B1 都為真,則為真。
- B0 -o B1
- 或者;如果 B0 或 B1 為真,則為真。
好的,現在我們知道如何評估一些條件。讓我們看看如何利用這種能力進行一些程式設計。
所有程式語言都需要兩件事:決策或條件執行形式,以及重複或迴圈形式。我們將在後面討論迴圈,現在讓我們關注條件執行。Bourne Shell 支援兩種條件執行形式,即 if 語句和 case 語句。
if 語句是兩者中最通用的。它的通用形式是
then command-list
elif command-list
then command-list
... else command-list
此命令應解釋如下
- 執行 if 後的命令列表。
- 如果最後一個命令返回狀態 0,則執行第一個 then 後的命令列表,並且語句在該列表中最後一個命令完成後終止。
- 如果最後一個命令返回非零狀態,則執行第一個 elif(如果有)後的命令列表。
- 如果最後一個命令返回狀態 0,則執行下一個 then 後的命令列表,並且語句在該列表中最後一個命令完成後終止。
- 如果最後一個命令返回非零狀態,則執行下一個 elif(如果有)後的命令列表,依此類推。
- 如果沒有 if 或 elif 後的命令列表以零狀態終止,則執行 else(如果有)後的命令列表。
- 語句終止。如果語句在沒有錯誤的情況下終止,則返回狀態為 0。
值得注意的是,if 語句允許在任何地方使用命令列表,包括在評估條件的地方。這意味著你可以在到達決策點之前執行任意數量的複合命令。影響決策結果的唯一命令是列表中執行的最後一個命令。
但是,在大多數情況下,為了提高可讀性和可維護性,你應該將自己限制為一個用於條件的命令。在大多數情況下,此命令將是 'test' 工具的使用。
rank=captain
if [ "$rank" = colonel ]
then
echo Hannibal Smith
elif [ "$rank" = captain ]
then
echo Howling Mad Murdock
elif [ "$rank" = lieutenant ]
then
echo Templeton Peck
else
echo B.A. Baracus
fi
case 語句類似於 if 語句的一種特殊形式,專門用於上一個示例中展示的測試型別:獲取一個值並將其與一組固定的預期值或模式進行比較。case 語句經常用於評估指令碼的命令列引數。例如,如果您編寫了一個使用開關來識別命令列引數的指令碼,您就會知道合法的開關數量有限。在這種情況下,case 語句是 if 語句的一個優雅的替代方案。
case 語句的一般形式是
pattern0 ) command-list-0 ;;
pattern1 ) command-list-1 ;;
...
該值可以是任何值,包括環境變數。每個模式都是一個正則表示式,執行的命令列表是第一個與該值匹配的模式的命令列表(所以確保你的模式沒有重疊)。每個命令列表必須以雙分號結束。如果語句在沒有語法錯誤的情況下終止,則返回狀態為零。
rank=captain
case $rank in
colonel) echo Hannibal Smith;;
captain) echo Howling Mad Murdock;;
lieutenant) echo Templeton Peck;;
sergeant) echo B.A. Baracus;;
*) echo OOPS;;
esac
If 與 case:有什麼區別?
[edit | edit source]那麼 if 和 case 語句到底有什麼區別?為什麼要有這兩個如此相似的語句呢?嗯,技術上的區別是這樣的:case 語句基於 shell 可用的資料(比如環境變數),而 if 語句基於程式或命令的退出狀態。由於固定值和環境變數依賴於 shell,而退出狀態是 Unix 系統通用的概念,這意味著 if 語句比 case 語句更通用。
讓我們來看一個稍微大一點的例子,把這兩個語句放在一起比較一下
#!/bin/sh
if [ "$2" ]
then
sentence="$1 is a"
else
echo Not enough command line arguments! >&2
exit 1
fi
case $2 in
fruit|veg*) sentence="$sentence vegetarian!";;
meat) sentence="$sentence meat eater!";;
*) sentence="${sentence}n omnivore!";;
esac
echo $sentence
請注意,這是一個 shell 指令碼,它使用位置變數來捕獲命令列引數。指令碼以 if 語句開頭,檢查我們是否擁有正確的引數數量——請注意使用 'test' 來檢視變數 $2 的值是否為空,以及 'test' 的退出狀態來確定 if 語句如何繼續。如果有足夠多的引數,我們假設第一個引數是一個名字,並開始構建指令碼結果的句子。否則,我們寫一條錯誤資訊(到 stderr,這是寫入錯誤的地方;在 檔案和流 中閱讀更多內容),並以非零返回值退出指令碼。請注意,這個 else 語句有一個包含多個命令的命令列表。
假設我們順利通過了 if 語句,我們就來到了 case 語句。在這裡,我們檢查變數 $2 的值,它應該是一個食物偏好。如果該值為 fruit 或以 veg 開頭的任何東西,我們向指令碼結果新增一個斷言,聲稱某人是素食主義者。如果該值為 exactly meat,該人是肉食主義者。其他任何東西,他都是雜食動物。請注意,在最後一個 case 模式子句中,我們必須在變數替換中使用花括號;這是因為我們想直接在 sentence 的現有值上新增一個字母 n,兩者之間沒有空格。
讓我們把指令碼放到一個名為 'preferences.sh' 的檔案中,看看對這個指令碼進行一些呼叫會產生什麼效果
$ sh preferences.sh
Not enough command line arguments!
$ sh preferences.sh Joe
Not enough command line arguments!
$ sh preferences.sh Joe fruit
Joe is a vegetarian!
$ sh preferences.sh Joe veg
Joe is a vegetarian!
$ sh preferences.sh Joe vegetables
Joe is a vegetarian!
$ sh preferences.sh Joe meat
Joe is a meat eater!
$ sh preferences.sh Joe meat potatoes
Joe is a meat eater!
$ sh preferences.sh Joe potatoes
Joe is an omnivore!
重複
[edit | edit source]除了條件執行機制外,每種程式語言都需要一種重複機制,即重複執行一組命令。Bourne Shell 為此提供了多種機制:while 語句、until 語句和 for 語句。
while 迴圈
[edit | edit source]while 語句是 Bourne shell 中最簡單、最直接的重複語句形式。它也是最通用的。其一般形式如下
do command-list2
done
while 語句的解釋如下
- 執行命令列表 1 中的命令。
- 如果最後一個命令的退出狀態為非零,則語句終止。
- 否則,執行命令列表 2 中的命令,並返回步驟 1。
- 如果語句不包含語法錯誤,並且它最終終止,則它將以退出狀態零終止。
與 if 語句非常相似,您可以使用完整的命令列表來控制 while 語句,並且只有該列表中的最後一個命令才能真正控制該語句。但在現實中,您可能希望將自己限制在一個命令,並且像 if 語句一樣,您通常會使用 'test' 程式來執行該命令。
counter=0
while [ $counter -lt 10 ]
do
echo $counter
counter=`expr $counter + 1`
done
1
2
3
4
5
6
7
8
9
while 語句通常用於處理指令碼可以有不定數量的命令列引數的情況,方法是使用 shift 命令和指示命令列引數數量的特殊變數 '$#'
#!/bin/sh
while [ $# -gt 0 ]
do
echo $1
shift
done
until 迴圈
[edit | edit source]until 語句也是一種重複語句,但它在語義上與 while 語句相反。until 語句的一般形式是
do command-list2
對該語句的解釋幾乎與 while 語句相同。唯一的區別是,只要命令列表 1 中的最後一個命令返回非零狀態,就執行命令列表 2 中的命令。或者,更簡單地說:只要迴圈條件沒有滿足,就執行命令列表 2。
雖然 while 語句主要用於建立某種效果(“重複執行直到完成”),但 until 語句更常用於輪詢某個條件的存在或等待某個條件滿足。例如,假設某個程序正在執行,該程序將把 10000 行寫入某個檔案。下面的 until 語句等待該檔案增長到 10000 行
until [ $lines -eq 10000 ]
do
lines=`wc -l dates | awk '{print $1}'`
sleep 5
done
for 迴圈
[edit | edit source]在關於 控制流 的部分,我們討論了 if 和 case 之間的區別,前者依賴於命令退出狀態,而後者與 shell 中可用的資料密切相關。這種配對也存在於重複語句中:while 和 until 使用命令退出狀態,而 for 使用 shell 中明確可用的資料。
for 語句遍歷一組固定的、有限的值。其一般形式是
do command-list
該語句為 'in' 之後命名的每個值執行命令列表。在命令列表中,"當前"值 wi 透過變數 name 可用。值列表必須用分號或換行符與 'do' 分隔。命令列表必須用分號或換行符與 'done' 分隔。例如
for myval in Abel Bertha Charlie Delta Easy Fox Gumbo Henry India
do
echo $myval Company
done
伯莎公司
查理公司
德爾塔公司
易公司
福克斯公司
甘博公司
亨利公司
印度公司
for 語句經常用於遍歷命令列引數。出於這個原因,shell 甚至為此用途提供了一種簡寫符號:如果您省略了 'in' 和值部分,該命令會將 $* 視為值列表。例如
#!/bin/sh
for arg
do
echo $arg
done
A
B
C
D
命令執行
[edit | edit source]在上一節關於 控制流 的內容中,我們討論了 Bourne Shell 提供的主要程式設計結構和控制流語句。然而,shell 中還有很多其他語法結構,允許您控制命令的執行方式,並將命令嵌入到其他命令中。在本節中,我們將討論其中一些更重要的結構。
命令連線
[edit | edit source]之前,我們已經瞭解了 if 語句作為條件執行的一種方法。除了這個擴充套件的語句外,Bourne Shell 還提供了一種將兩個命令直接連線起來的方法,使其中一個命令的執行取決於另一個命令的結果(退出狀態)。這對於對命令執行進行快速、內聯的決策非常有用。但是您可能不想在 shell 指令碼或更長的命令序列中使用這些結構,因為它們的可讀性不是很好。
您可以使用 && 和 || 運算子將命令連線在一起。這些運算子(您可能會認識到它們是從 C 程式語言借來的)是短路運算子:它們使第二個命令的執行依賴於第一個命令的退出狀態,因此您可以避免不必要的命令執行。
&& 運算子將兩個命令連線在一起,只有當第一個命令的退出狀態為零(即第一個命令“成功”)時,才會執行第二個命令。請看以下示例
echo Hello World > tempfile.txt && rm tempfile.txt
在這個例子中,如果檔案建立失敗(例如,因為檔案系統是隻讀的),那麼刪除將毫無意義。使用&& 運算子可以防止在檔案建立失敗的情況下嘗試刪除。一個類似的,可能更有用的例子是這個
test -f my_important_file && cp my_important_file backup
與&& 運算子相反,|| 運算子僅當第一個命令的退出狀態不為零(即失敗)時,才會執行第二個命令。請看以下示例
test -f my_file || echo Hello World > my_file
對於這兩個運算子,連線的命令的退出狀態是實際執行的最後一個命令的退出狀態。
您可以使用; 運算子將多個命令連線到一個命令列表中,如下所示
mkdir newdir;cd newdir
這裡沒有條件執行;所有命令都會執行,即使其中一個命令失敗。
將命令連線到命令列表時,可以將命令分組在一起以提高畫質晰度和一些特殊處理。有兩種方法可以對命令進行分組:使用大括號和使用圓括號。
使用大括號進行分組可以增強清晰度。使用它們不會為使用分號或換行符連線新增任何語義,但是您必須在命令列表後插入一個額外的分號或換行符。大括號和命令列表之間的空格對於 shell 識別分組是必需的。以下是一個示例
{ mkdir newdir;cd newdir; }
或
{
mkdir newdir
cd newdir
}
大括號還可以用來將命令分組在一起,以將它們整合到管道中並重定向它們的輸入或輸出。這與在相同位置使用函式完全相同。
stderr。首先使用函式,然後使用組。dappend() {
date
cat
}
echo "Hello, today's world" | dappend 1>&2
或
echo "Hello, today's world" | { date;cat; } 1>&2
圓括號更有趣。當您使用圓括號對命令列表進行分組時,它將在一個單獨的程序中執行。這意味著您在命令列表中所做的任何事情都不會影響您發出命令的環境。再次考慮上面的例子,使用大括號和圓括號
再舉一個例子
$ VAR0=A
$ (VAR1=B)
$ echo \"$VAR0\" \"$VAR1\"
在關於環境的章節中,我們討論了變數替換。Bourne Shell 還支援命令替換。這有點像變數替換,但不是用變數的值替換變數,而是用命令的輸出替換命令。我們在之前討論while語句時看到了一個例子,我們用算術表示式計算的結果來分配環境變數。
命令替換是使用兩種表示法中的任何一種完成的。原始 Bourne Shell 使用重音符 (`command`),它通常仍受大多數 shell 支援。後來 POSIX 1003.1 標準添加了$( command ) 表示法。請看以下示例
cp myfile backup/myfile-`date`
cp myfile backup/myfile-$(date)
通常,在您使用 shell 執行的日常任務中,您希望明確準確地說明要操作哪些檔案。畢竟,您希望刪除一個特定檔案,而不是隨機檔案。您希望將網路通訊傳送到網路裝置檔案,而不是傳送到鍵盤。
但是,有時,尤其是在指令碼中,您需要能夠一次操作多個檔案。例如,如果您編寫了一個指令碼,定期備份主目錄中所有以“.dat”結尾的檔案。如果這些檔案很多,或者每天都會建立更多新的檔案,每次都有新的名稱,那麼您不想在備份指令碼中顯式地命名所有這些檔案。
我們還看到了另一個不想過於明確的例子:在關於case語句的部分中,有一個例子表明,如果有人喜歡水果或以“veg”開頭的任何東西,那麼這個人就是一個素食主義者。我們可以在那裡包含各種各樣的選項,並且明確(儘管您可以用“veg”開頭製作無限多個單詞)。但是我們使用了一個模式,省去了很多時間。
對於這些確切的情況,shell 支援正則表示式的(有限)形式:允許您說類似於“我的意思是每個字串,每個字元序列,看起來有點像這樣”之類的東西。shell 允許您在任何地方使用這些正則表示式(儘管它們並不總是合理 - 例如,使用正則表示式來指定要複製檔案的位置是沒有意義的)。這意味著在 shell 指令碼中,在互動式 shell 中,作為case語句的一部分,用於選擇檔案、通用字串、任何東西。
為了建立正則表示式,您將使用一個或多個元字元。元字元是 shell 有特殊意義的字元,並自動被識別為正則表示式的一部分。Bourne shell 識別以下元字元
- *
- 匹配任何字串。
- ?
- 匹配任何單個字元。
- [characters]
- 匹配尖括號中包含的任何字元。
- [!characters]
- 匹配尖括號中未包含的任何字元。
- pat0|pat1
- 匹配與pat0 或 pat1 匹配的任何字串(僅在case語句模式中!)。
以下是一些示例,說明您如何在 shell 中使用正則表示式
ls *.dat
ls file-??.txt
for i in *.txt; do cp $i backup/$i-`date +%Y%m%d`; done
ls Backup[01]
ls Backup[!01]
myscript*.sh
在選擇檔案時,元字元匹配所有檔案,除了名稱以句點(“.”)開頭的檔案。以句點開頭的檔案要麼是特殊檔案,要麼被認為是配置檔案。出於這個原因,這些檔案是半受保護的,因為您不能僅僅使用元字元來選擇它們。為了在使用正則表示式選擇時包含這些檔案,您必須顯式地包含開頭的句點。例如
上面的示例顯示了句點檔案的列表。在這個示例中,列表包括“.profile”,它是 Bourne Shell 的使用者配置檔案。它還包括特殊目錄“.”(表示“當前目錄”)和“..”(表示當前目錄的父目錄)。您可以像其他任何目錄一樣訪問這些特殊目錄。所以例如
在語義上與“ls”相同,而
將您的工作目錄更改為之前作為工作目錄的目錄的父目錄。
當您引入像上一節中討論的元字元這樣的特殊字元時,您會自動進入真正不希望這些特殊字元被評估的情況。例如,假設您有一個檔案,其名稱包含一個星號('*')。您將如何訪問該檔案?例如
echo Test0 > asterisk*.file
echo Test1 > asteriskSTAR.file
cat asterisk*.file
Test1
顯然,需要一種方法來臨時關閉元字元。Bourne Shell 內建的引用機制可以做到這一點。事實上,它們的功能遠不止於此。例如,如果您有一個檔名中包含空格的檔案(因此 shell 無法判斷檔名中的不同單詞屬於一起),引用機制將幫助您解決這個問題。
Bourne Shell 中有三種引用機制
- \
- 反斜槓,用於單字元引用。
- ''
- 單引號,用於引用整個字串。
- ""
- 雙引號,用於引用整個字串,但仍允許某些特殊字元。
其中最簡單的是反斜槓,它引用緊隨其後的字元。所以,例如
echo *
ional1.sh~ conditional.sh conditional.sh~ dates error_test.sh error_test.sh~ fil
e with spaces.txt looping0.sh looping1.sh out_nok out_ok preferences.sh pre
ferences.sh~ test.txt
因此反斜槓基本上在單個字元的持續時間內停用特殊字元解釋。有趣的是,換行符在此上下文中也被認為是一個特殊字元,因此您可以使用反斜槓將命令拆分為多行,以便直譯器理解。就像這樣
反斜槓轉義符也適用於包含空格的檔名
ls file with spaces.txt
ls: 無法訪問 with: 沒有此檔案或目錄
ls: 無法訪問 spaces.txt: 沒有此檔案或目錄
但是,如果你想將反斜槓傳遞給 shell 呢?想想看,反斜槓會停用對單個字元的解釋,所以如果你想將反斜槓用於其他用途... 那麼 '\\' 可以做到!
所以我們看到,反斜槓允許你透過引用來停用對單個字元的特殊字元解釋。但是,如果你想一次引用很多特殊字元呢?正如你在上面帶空格的檔名中看到的,你可以分別引用每個特殊字元,但這很快就會變得很麻煩。通常,直接引用整個字元字串更快、更簡單,並且更容易避免錯誤。要做到這一點,你需要使用單引號。兩個單引號引用它們包圍的整個字串,停用對該字串中所有特殊字元的解釋 - 除了單引號本身(這樣你就可以停止引用)。例如
讓我們嘗試一下。假設出於某種奇怪的原因,我們想列印三個星號("***"),然後是一個空格,然後是當前工作目錄,再是一個空格,最後是三個星號。我們知道可以用單引號停用元字元的解釋,所以這應該沒什麼大不了的,對吧?為了方便起見,內建命令 'pwd' 會print working directory,所以這真的很容易
到底哪裡錯了?嗯,單引號會停用對所有特殊字元的解釋。所以我們用於命令替換的反引號不起作用!我們可以用其他方法來實現嗎?例如使用工作目錄環境變數($PWD)?不行,$-字元也不起作用。
這是一個典型的“金髮姑娘”問題。我們想要引用一些特殊字元,但不是全部。我們可以使用反斜槓,但這不夠方便(太冷了)。我們可以使用單引號,但這會停用太多特殊字元(太熱了)。我們需要的是恰到好處的引用。更確切地說,我們想要(而且比你想象的更頻繁)的是停用所有特殊字元解釋,除了變數和命令替換。由於這是一個常見的需求,shell 透過一個單獨的引用機制來支援它:雙引號。雙引號會停用所有特殊字元解釋,除了反引號(命令替換)、$(變數替換)和雙引號本身(這樣你就可以停止引用)。所以我們上面問題的解決方案是
順便說一句,為了教學目的,我們在上面實際上稍微作弊了一下(嘿,你試試想出這些例子);我們也可以這樣解決這個問題