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 變數中)的形式返回這些值:true為零,false為其他值。test 命令的一般形式是
例如
test "Hello World" = "Hello World"
此測試用於檢查兩個字串是否相等,返回退出狀態為零。還有一個'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 之後的命令列表。
- 如果最後一個命令返回狀態零,則執行第一個 then 之後的命令列表,並在該列表中的最後一個命令完成執行後終止該語句。
- 如果最後一個命令返回非零狀態,則執行第一個 elif(如果有)之後的命令列表。
- 如果最後一個命令返回狀態零,則執行下一個 then 之後的命令列表,並在該列表中的最後一個命令完成執行後終止該語句。
- 如果最後一個命令返回非零狀態,則執行下一個 elif(如果有)之後的命令列表,依此類推。
- 如果 if 或 elif 之後的任何命令列表都沒有以零狀態終止,則執行 else(如果有)之後的命令列表。
- 該語句終止。如果該語句在沒有錯誤的情況下終止,則返回狀態為零。
有趣的是,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 語句之間究竟有什麼區別?以及為什麼要有兩種如此相似的語句?嗯,技術上的區別在於: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 的值,它應該是一個食物偏好。如果該值為水果或以 veg 開頭的任何東西,我們將向指令碼結果新增一個斷言,表明某人是一個素食主義者。如果該值恰好是肉類,那麼這個人是一個肉食者。其他任何情況,他都是一個雜食動物。請注意,在最後一個 case 模式子句中,我們必須在變數替換中使用大括號;這是因為我們希望直接在句子的現有值上新增一個字母 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!
除了條件執行機制外,每種程式語言都需要一種重複機制,即重複執行一組命令。Bourne Shell 提供了幾種用於此目的的機制:while 語句、until 語句和 for 語句。
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 語句也是一個重複語句,但它是 while 語句的語義相反。until 語句的通用形式為
do command-list2
do command-list2
done
until [ $lines -eq 10000 ]
do
lines=`wc -l dates | awk '{print $1}'`
sleep 5
done
while 語句主要用於建立某種效果(“重複直到完成”),而 until 語句更常用於輪詢某個條件的存在或等待某個條件滿足。例如,假設某個正在執行的程序將 10000 行寫入某個特定檔案。以下 until 語句將等待該檔案增長到 10000 行。
等待 myfile.txt 增長到 10000 行。for 迴圈
for 語句迴圈遍歷一組固定的、有限的值。它的通用形式為
for name in w1 w2 ...
for myval in Abel Bertha Charlie Delta Easy Fox Gumbo Henry India
do
echo $myval Company
done
該語句對“in”之後的每個命名值為命令列表執行。在命令列表中,可以透過變數name來獲取“當前”值wi。值列表必須透過分號或換行符與“do”分隔。並且命令列表必須透過分號或換行符與“done”分隔。因此,例如
一個列印一些值的 for 迴圈
Abel Company
Bertha Company
Charlie Company
Delta Company
Easy Company
Fox Company
Gumbo Company
#!/bin/sh
for arg
do
echo $arg
done
for 語句被大量用於迴圈遍歷命令列引數。為此,shell 甚至為此用途提供了一種簡寫表示法:如果您省略“in”和值部分,則該命令將假定“$*”作為值列表。例如
使用 for 迴圈遍歷命令列引數
$ sh loop_args.sh A B C D
A
C
Dfor 的這種用法通常與 case 結合使用,以處理命令列開關。
在上一節關於控制流的內容中,我們討論了 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 識別以下元字元
- *
- 匹配任何字串。
- ?
- 匹配任何單個字元。
- [字元]
- 匹配尖括號中包含的任何字元。
- [!字元]
- 匹配尖括號中不包含的任何字元。
- 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
因此,反斜槓基本上停用了對單個字元的特殊字元解釋。有趣的是,換行符在此上下文中也被視為特殊字元,因此您可以使用反斜槓將命令拆分為多行,並將其解釋給直譯器。像這樣
echo This is a \
>very long command!
反斜槓轉義也適用於包含空格的檔名
ls file with spaces.txt
ls: 無法訪問 with: 沒有此檔案或目錄
ls: 無法訪問 spaces.txt: 沒有此檔案或目錄
但是,如果您想將反斜槓傳遞給 shell 呢?想想看。反斜槓會停用對單個字元的解釋,所以如果您想將反斜槓用於其他用途... 那麼 '\\' 就行了!
所以我們已經看到,反斜槓允許您透過引用來停用對單個字元的特殊字元解釋。但是,如果您想一次引用多個特殊字元呢?正如您在上面帶有空格的檔名中看到的,您可以單獨引用每個特殊字元,但這很快就會變得很麻煩。通常,一次引用整個字串字元更快、更容易且不易出錯。為此,您使用單引號。兩個單引號引用它們包圍的整個字串,停用對該字串中所有特殊字元的解釋——除了單引號(這樣您就可以停止引用)。例如
所以我們嘗試一下。假設由於某種奇怪的原因,我們想列印三個星號("***"),然後是一個空格,然後是當前工作目錄,一個空格,然後是另外三個星號。我們知道可以使用單引號停用元字元解釋,所以這應該沒什麼大不了,對吧?為了使生活更輕鬆,內建命令 'pwd' printworkingdirectory,所以這真的很容易
所以哪裡出錯了?嗯,單引號會停用對所有特殊字元的解釋。所以我們用於命令替換的反引號不起作用!我們能用其他方法嗎?比如使用工作目錄環境變數($PWD)?不,$-字元也不起作用。
這是一個典型的“金髮姑娘”問題。我們想引用一些特殊字元,但不是全部。我們可以使用反斜槓,但這不足以方便(太冷)。我們可以使用單引號,但這會停用太多特殊字元(太熱)。我們需要的是恰到好處的引用。更確切地說,我們想要(而且比你想象的更頻繁)停用所有特殊字元解釋,除了變數和命令替換。因為這是一種常見的需求,shell 透過一個單獨的引用機制來支援它:雙引號。雙引號會停用所有特殊字元解釋,除了反引號(命令替換)、$(變數替換)和雙引號(這樣您就可以停止引用)。所以上面我們問題的解決方案是
順便說一句,為了教學目的,我們在上面實際上有點作弊(嘿,你試試想出這些例子);我們也可以這樣解決問題