官术网_书友最值得收藏!

2.6 shell的語法

現在你已看過一個簡單的shell程序示例,是時候來深入研究shell強大的程序設計能力了。shell是一種很容易學習的程序設計語言,它可以在把各個小程序段組合為一個大程序之前就能很容易地對它們分別進行交互式的測試。你還可以用bash shell編寫出相當龐大的結構化程序。在接下來的幾節里,我們將學習以下內容:

? 變量:字符串、數字、環境和參數

? 條件:shell中的布爾值

? 程序控制:if、elif、for、while、until、case

? 命令列表

? 函數

? shell內置命令

? 獲取命令的執行結果

? here文檔

2.6.1 變量

在shell里,使用變量之前通常并不需要事先為它們做出聲明。你只是通過使用它們(比如當你給它們賦初始值時)來創建它們。在默認情況下,所有變量都被看作字符串并以字符串來存儲,即使它們被賦值為數值時也是如此。shell和一些工具程序會在需要時把數值型字符串轉換為對應的數值以對它們進行操作。Linux是一個區分大小寫的系統,因此shell認為變量foo與Foo是不同的,而這兩者與FOO又是不同的。

在shell中,你可以通過在變量名前加一個$符號來訪問它的內容。無論何時你想要獲取變量內容,你都必須在它前面加一個$字符。當你為變量賦值時,你只需要使用變量名,該變量會根據需要被自動創建。一種檢查變量內容的簡單方式就是在變量名前加一個$符號,再用echo命令將它的內容輸出到終端上。

在命令行上,你可以通過設置和檢查變量salutation的不同值來實際查看變量的使用:

注意,如果字符串里包含空格,就必須用引號把它們括起來。此外,等號兩邊不能有空格。

你可以使用read命令將用戶的輸入賦值給一個變量。這個命令需要一個參數,即準備讀入用戶輸入數據的變量名,然后它會等待用戶輸入數據。通常情況下,在用戶按下回車鍵時,read命令結束。當從終端上讀取一個變量時,你一般不需要使用引號,如下所示:

1.使用引號

在繼續學習之前,你先需要弄清楚shell的一個特點:引號的使用。

一般情況下,腳本文件中的參數以空白字符分隔(例如,一個空格、一個制表符或者一個換行符)。如果你想在一個參數中包含一個或多個空白字符,你就必須給參數加上引號。

像$foo這樣的變量在引號中的行為取決于你所使用的引號類型。如果你把一個$變量表達式放在雙引號中,程序執行到這一行時就會把變量替換為它的值;如果你把它放在單引號中,就不會發生替換現象。你還可以通過在$字符前面加上一個\字符以取消它的特殊含義。

字符串通常都被放在雙引號中,以防止變量被空白字符分開,同時又允許$擴展。

實驗 變量的使用

這個例子顯示了引號在變量輸出中的作用:

輸出結果如下:

實驗解析

變量myvar在創建時被賦值字符串Hi there。你用echo命令顯示該變量的內容,同時顯示了在變量名前加一個$符號就能得到變量的內容。你看到使用雙引號并不影響變量的替換,但使用單引號和反斜線就不進行變量的替換。你還使用read命令從用戶那里讀入一個字符串。

2.環境變量

當一個shell腳本程序開始執行時,一些變量會根據環境設置中的值進行初始化。這些變量通常用大寫字母做名字,以便把它們和用戶在腳本程序里定義的變量區分開來,后者按慣例都用小寫字母做名字。具體創建的變量取決于你的個人配置。在系統的使用手冊中列出了許多這樣的環境變量,表2-2列出了其中一些主要的變量。

表2-2

如果想通過執行env <command>命令來查看程序在不同環境下是如何工作的,請查閱env命令的使用手冊。你也將在本章的后面看到如何使用export命令在子shell中設置環境變量。

3.參數變量

如果腳本程序在調用時帶有參數,一些額外的變量就會被創建。即使沒有傳遞任何參數,環境變量$#也依然存在,只不過它的值是0罷了。

參數變量見表2-3。

表2-3

通過下面的例子,你可以很容易地看出$@和$*之間的區別:

如你所見,雙引號里面的$@把各個參數擴展為彼此分開的域,而不受IFS值的影響。一般來說,如果你想訪問腳本程序的參數,使用$@是明智的選擇。

除了使用echo命令查看變量的內容外,你還可以使用read命令來讀取它們。

實驗 使用參數和環境變量

下面的腳本程序演示了一些簡單的變量操作。當輸入腳本程序的內容并把它保存為文件try_var后,別忘了用chmod+x try_var命令把它設置為可執行。

運行這個腳本程序,你將得到如下所示的輸出結果:

實驗解析

這個腳本程序創建變量salutation并顯示它的內容,然后顯示各種參數變量以及環境變量$HOME都已存在并有了適當的值。

我們將在后面進一步介紹參數替換。

2.6.2 條件

所有程序設計語言的基礎是對條件進行測試判斷,并根據測試結果采取不同行動的能力。在討論它之前,我們先來看看在shell腳本程序里可以使用的條件結構,然后再來看看使用這些條件的控制結構。

一個shell腳本能夠對任何可以從命令行上調用的命令的退出碼進行測試,其中也包括你自己編寫的腳本程序。這也就是為什么要在所有自己編寫的腳本程序的結尾包括一條返回值的exit命令的重要原因。

test或[命令

在實際工作中,大多數腳本程序都會廣泛使用shell的布爾判斷命令[或test。在一些系統上,這兩個命令的作用是一樣的,只是為了增強可讀性,當使用[命令時,我們還使用符號]來結尾。把[符號當作一條命令多少有點奇怪,但它在代碼中確實會使命令的語法看起來更簡單、更明確、更像其他的程序設計語言。

在一些老版本的UNIX shell中,這些命令調用的是一個外部程序,但在較新的shell版本中,它們已成為shell的內置命令。我們將在本章后面介紹各種命令時再次討論這個問題。

因為test命令在shell腳本程序以外用得很少,所以那些很少編寫shell腳本的Linux用戶往往會將自己編寫的簡單程序命名為test。如果程序不能正常工作,很可能是因為它與shell中的test命令發生了沖突。要想查看系統中是否有一個指定名稱的外部命令,你可以嘗試使用which test這樣的命令來檢查執行的是哪一個test命令,或者使用./test這種執行方式以確保你執行的是當前目錄下的腳本程序。如有疑問,你只需養成在調用腳本的前面加上./的習慣即可。

我們以一個最簡單的條件為例來介紹test命令的用法:檢查一個文件是否存在。用于實現這一操作的命令是test -f <filename>,所以在腳本程序里,你可以寫出如下所示的代碼:

你還可以寫成下面這樣:

test命令的退出碼(表明條件是否被滿足)決定是否需要執行后面的條件代碼。

注意,你必須在[符號和被檢查的條件之間留出空格。要記住這一點,你可以把[符號看作和test命令一樣,而test命令之后總是應該有一個空格。

如果你喜歡把then和if放在同一行上,就必須要用一個分號把test語句和then分隔開。如下所示:

test命令可以使用的條件類型可以歸為3類:字符串比較、算術比較和與文件有關的條件測試,表2-4、表2-5和表2-6描述了這3種條件類型。

表2-4

表2-5

表2-6

讀者可能想知道什么是set-group-id和set-user-id(也叫做set-gid和set-uid)位。set-uid位授予了程序其擁有者的訪問權限而不是其使用者的訪問權限,而set-gid位授予了程序其所在組的訪問權限。這兩個特殊位是通過chmod命令的選項u和g設置的。set-gid和set-uid標志對shell腳本程序不起作用,它們只對可執行的二進制文件有用。

我們稍微超前了一些,但是接下來的測試/bin/bash文件狀態的例子可以讓你看出如何使用它們:

各種與文件有關的條件測試的結果為真的前提是文件必須存在。上述列表僅僅列出了test命令比較常用的選項,完整的選項清單請查閱它的使用手冊。如果你使用的是bash,那么test命令是shell的內置命令,使用help test命令可以獲得test命令更詳細的信息。我們將在本章后面用到這里給出的部分選項。

現在你已學習了“條件”,下面你將看到使用它們的控制結構。

2.6.3 控制結構

shell有一組控制結構,它們與其他程序設計語言中的控制結構很相似。

在下面的各小節中,各語句的語法中的statements表示when、while或until測試條件滿足時,將要執行的一系列命令。

1.if語句

if語句非常簡單:它對某個命令的執行結果進行測試,然后根據測試結果有條件地執行一組語句。如下所示:

實驗 使用if語句

if語句的一個常見用法是提一個問題,然后根據回答作出決定,如下所示:

這將給出如下所示的輸出:

這個腳本程序用[命令對變量timeofday的內容進行測試,測試結果由if命令判斷,由它來決定執行哪部分代碼。

請注意,你用額外的空白符來縮進if結構內部的語句。這只是為了照顧人們的閱讀習慣,shell會忽略這些多余的空白符。

2.elif語句

遺憾的是,上面這個非常簡單的腳本程序存在幾個問題。其中一個問題是,它會把所有不是yes的回答都看做是no。你可以通過使用elif結構來避免出現這樣的情況,它允許你在if結構的else部分被執行時增加第二個檢查條件。

實驗 用elif結構做進一步檢查

你可以對上面的腳本程序做些修改,讓它在用戶輸入yes或no以外的其他任何數據時報告一條出錯信息。這是通過將else替換為elif并且增加另一個測試條件的方法來完成的。

實驗解析

這個腳本程序與上一個例子很相似,但新增的elif命令會在第一個if條件不滿足的情況下進一步測試變量。如果兩次測試的結果都不成功,就打印一條出錯信息并以1為退出碼結束腳本程序,調用者可以在調用程序中利用這個退出碼來檢查腳本程序是否執行成功。

3.一個與變量有關的問題

剛才所做的修改彌補了一個非常明顯的缺陷,但這個腳本程序還潛藏著一個更隱蔽的問題。運行這個新的腳本程序,但是這次不回答問題,而是直接按下回車鍵(或是某些鍵盤上的Return鍵)。你將看到如下所示的出錯信息;

哪里出問題了呢?問題就在第一個if語句中。在對變量timeofday進行測試的時候,它包含一個空字符串,這使得if語句成為下面這個樣子:

而這不是一個合法的條件。為了避免出現這種情況,你必須給變量加上引號,如下所示:

這樣,一個空變量提供的就是一個合法的測試了:

新腳本程序如下所示:

這個腳本對用戶直接按下回車鍵來回答問題的情況也能夠應付自如了。

如果你想讓echo命令去掉每一行后面的換行符,可移植性最好的辦法是使用printf命令(請見本章后面的printf一節)而不是echo命令。有的shell用echo-e命令來完成這一任務,但并不是所有的系統都支持該命令。bash使用echo-n命令來去除換行符,所以如果確信自己的腳本程序只運行在bash上,你就可以使用如下的語法:

請注意,你需要在結束引號前留出一個額外的空格,這使得在用戶輸入響應前有一個間隙,從而看起來更加整潔。

4.for語句

我們可以用for結構來循環處理一組值,這組值可以是任意字符串的集合。它們可以在程序里被列出,更常見的做法是使用shell的文件名擴展結果。

它的語法很簡單:

實驗 使用固定字符串的for循環

循環值通常是字符串,所以你可以這樣寫程序:

該程序的輸出結果如下所示:

如果你把第一行由for foo in bar fud 43修改為for foo in "bar fud 43"會怎樣呢?別忘了,加上引號就等于告訴shell把引號之間的一切東西都看作是一個字符串。這是在變量里保留空格的一種辦法。

實驗解析

這個例子創建了一個變量foo,然后在for循環里每次給它賦一個不同的值。因為shell在默認情況下認為所有變量包含的都是字符串,所以字符串43在使用中與字符串fud是一樣合法有效的。

實驗 使用通配符擴展的for循環

正如我們前面所提到的,for循環經常與shell的文件名擴展一起使用。這意味著在字符串的值中使用一個通配符,并由shell在程序執行時填寫出所有的值。

你已經在最早的first例子中見過這種做法了。該腳本程序用shell擴展把*擴展為當前目錄中所有文件的名字,然后它們依次作為for循環中的變量$file使用。

我們來快速地看看另外一個通配符擴展的例子。假設你想打印當前目錄中所有以字母f開頭的腳本文件,并且你知道自己的所有腳本程序都以.sh結尾,你就可以這樣做:

實驗解析

這個例子演示了$(command)語法的用法,我們將在后面的內容中對它做更詳細地介紹(參見2.6.6節)。簡單地說,for命令的參數表來自括在$( )中的命令的輸出結果。

shell擴展f*.sh給出所有匹配此模式的文件的名字。

請記住,shell腳本程序中所有的變量擴展都是在腳本程序被執行時而不是在編寫它時完成的。所以,變量聲明中的語法錯誤只有在執行時才會被發現,就像前面我們給空變量加引號的例子中看到的那樣。

5.while語句

因為在默認情況下,所有的shell變量值都被認為是字符串,所以for循環特別適合于對一系列字符串進行循環處理,但如果你事先并不知道循環要執行的次數,那么它就顯得不是那么有用了。

如果需要重復執行一個命令序列,但事先又不知道這個命令序列應該執行的次數,你通常會使用一個while循環,它的語法如下所示:

請看下面的例子,這是一個非常簡陋的密碼檢查程序:

這個腳本程序的一個輸出示例如下所示:

很明顯,這不是一種詢問密碼的非常安全的辦法,但它確實演示了while語句的作用。do和done之間的語句將反復執行,直到條件不再為真。在這個例子中,你檢查的條件是變量trythis的值是否等于secret。循環將一直執行直到$trythis等于secret。隨后你將繼續執行腳本程序中緊跟在done后面的語句。

6.until語句

until語句的語法如下所示:

它與while循環很相似,只是把條件測試反過來了。換句話說,循環將反復執行直到條件為真,而不是在條件為真時反復執行。

一般來說,如果需要循環至少執行一次,那么就使用while循環;如果可能根本都不需要執行循環,就使用until循環。

下面是一個until循環的例子,你設置一個警報,當某個特定的用戶登錄時,該警報就會啟動,你通過命令行將用戶名傳遞給腳本程序。如下所示:

如果用戶已經登錄,那么循環就不需要執行。所以在這種情況下,使用until語句比使用while語句更自然。

7.case語句

case結構比你目前為止見過的其他結構都要稍微復雜一些。它的語法如下所示:

這看上去有些令人生畏,但case結構允許你通過一種比較復雜的方式將變量的內容和模式進行匹配,然后再根據匹配的模式去執行不同的代碼。這要比使用多條if、elif和else語句來執行多個條件檢查要簡單得多。


請注意,每個模式行都以雙分號(; ;)結尾。因為你可以在前后模式之間放置多條語句,所以需要使用一個雙分號來標記前一個語句的結束和后一個模式的開始。


因為case結構具備匹配多個模式然后執行多條相關語句的能力,這使得它非常適合于處理用戶的輸入。弄明白case工作原理的最好方法就是通過例子來進行說明。我們將使用3個實驗例子逐步深入地對它進行介紹,每次都對模式匹配進行改進。

你在case結構的模式中使用如*這樣的通配符時要小心。因為case將使用第一個匹配的模式,即使后續的模式有更加精確的匹配也是如此。

實驗case示例一:用戶輸入

你可以用case結構編寫一個新版的輸入測試腳本程序,讓它更具選擇性并且對非預期輸入也更寬容:

實驗解析

當case語句被執行時,它會把變量timeofday的內容與各字符串依次進行比較。一旦某個字符串與輸入匹配成功,case命令就會執行緊隨右括號)后面的代碼,然后就結束。

case命令會對用來做比較的字符串進行正常的通配符擴展,因此你可以指定字符串的一部分并在其后加上一個*通配符。只使用一個單獨的*表示匹配任何可能的字符串,所以我們總是在其他匹配字符串之后再加上一個*以確保如果沒有字符串得到匹配,case語句也會執行某個默認動作。之所以能夠這樣做是因為case語句是按順序比較每一個字符串,它不會去查找最佳匹配,而僅僅是查找第一個匹配。因為默認條件通常都是些“最不可能出現”的條件,所以使用*對腳本程序的調試很有幫助。

實驗case示例二:合并匹配模式

上面例子中的case結構明顯比多個if語句的版本更精致,但通過合并匹配模式,你可以編寫一個更加清晰的版本。如下所示:

實驗解析

這個腳本程序在每個case條目中都使用了多個字符串,case將對每個條目中的多個不同的字符串進行測試,以決定是否需要執行相應的語句。這使得腳本程序不僅長度變短,而且實際上也更容易閱讀。這個腳本程序同時還顯示了*通配符的用法,雖然這樣做有可能匹配意料之外的模式。例如,如果用戶輸入never,它就會匹配n*并顯示出Good Afternoon,而這并不是我們希望的行為。另外需要注意的是*通配符擴展在引號中不起作用。

實驗case示例三:執行多條語句

最后,為了讓這個腳本程序具備可重用性,你需要在使用默認模式時給出另外一個退出碼。如下所示:

實驗解析

為了演示模式匹配的不同用法,這個代碼改變了no情況下的匹配方法。你還看到了如何在case語句中為每個模式執行多條語句。注意,你必須很小心地把最精確的匹配放在最開始,而把最一般化的匹配放在最后。這樣做很重要,因為case將執行它找到的第一個匹配而不是最佳匹配。如果你把*)放在開頭,那不管用戶輸入的是什么,都會匹配這個模式。


請注意,esac前面的雙分號(; ;)是可選的。在C語言程序設計中,即使少一個break語句都算是不好的程序設計做法,但在shell程序設計中,如果最后一個case模式是默認模式,那么省略最后一個雙分號(; ;)是沒有問題的,因為后面沒有其他的case模式需要考慮了。

為了讓case的匹配功能更強大,你可以使用如下的模式:

這限制了允許出現的字母,但它同時也允許多種多樣的答案并且提供了比*通配符更多的控制。

8.命令列表

有時,你想要將幾條命令連接成一個序列。例如,你可能想在執行某個語句之前同時滿足好幾個不同的條件,如下所示:

或者你可能希望至少在這一系列條件中有一個為真,像下面這樣:

雖然這可以通過使用多個if語句來實現,但如你所見,寫出來的程序非常笨拙。shell提供了一對特殊的結構,專門用于處理命令列表,它們是AND列表和OR列表。雖然它們通常在一起使用,但我們將分別介紹它們的語法。

AND列表

AND列表結構允許你按照這樣的方式執行一系列命令:只有在前面所有的命令都執行成功的情況下才執行后一條命令。它的語法是:

從左開始順序執行每條命令,如果一條命令返回的是true,它右邊的下一條命令才能夠執行。如此持續直到有一條命令返回false,或者列表中的所有命令都執行完畢。&&的作用是檢查前一條命令的返回值。

每條語句都是獨立執行,這就允許你把許多不同的命令混合在一個單獨的命令列表中,就像下面的腳本程序顯示的那樣。AND列表作為一個整體,只有在列表中的所有命令都執行成功時,才算它執行成功,否則就算它失敗。

實驗AND列表

在下面的腳本程序中,你執行touch file_one命令(檢查文件是否存在,如果不存在就創建它)并刪除file_two文件。然后用AND列表檢查每個文件是否存在并通過echo命令給出相應的指示。

執行這個腳本程序,你將看到如下所示的結果:

實驗解析

touch和rm命令確保當前目錄中的有關文件處于已知狀態。然后&&列表執行[-f file_one]語句,這條語句肯定會執行成功,因為你已經確保該文件是存在的了。因為前一條命令執行成功,所以echo命令得以執行,它也執行成功(echo命令總是返回true)。當執行第三個測試[-f file_two]時,因為該文件并不存在,所以它執行失敗了。這條命令的失敗導致最后一條echo語句未被執行。而因為該命令列表中的一條命令失敗了,所以&&列表的總的執行結果是false, if語句將執行它的else部分。

OR列表

OR列表結構允許我們持續執行一系列命令直到有一條命令成功為止,其后的命令將不再被執行。它的語法是:

從左開始順序執行每條命令。如果一條命令返回的是false,它右邊的下一條命令才能夠被執行。如此持續直到有一條命令返回true,或者列表中的所有命令都執行完畢。

||列表和&&列表很相似,只是繼續執行下一條語句的條件現在變為其前一條語句必須執行失敗。

實驗OR列表

沿用上一個例子,但要修改下面程序清單里陰影部分的語句:

這個腳本程序的輸出是:

實驗解析

頭兩行代碼簡單的為腳本程序的剩余部分設置好相應的文件。第一條命令[-f file_one]失敗了,因為這個文件不存在。接下來執行echo語句,它返回true,因此||列表中的后續命令將不會被執行,因為||列表中有一條命令(echo)返回的是true,所以if語句執行成功并將執行其then部分。

這兩種結構的返回結果都等于最后一條執行語句的返回結果。

這些列表類型結構的執行方式與C語言中對多個條件進行測試的執行方式很相似。只需執行最少的語句就可以確定其返回結果。不影響返回結果的語句不會被執行。這通常被稱為短路求值(short circuit evaluation)。

將這兩種結構結合在一起將更能體現邏輯的魅力。請看:

在上面的語句中,如果測試成功就會執行第一條命令,否則執行第二條命令。你最好用這些不尋常的命令列表來進行實驗,但在通常情況下,你應該用括號來強制求值的順序。

9.語句塊

如果你想在某些只允許使用單個語句的地方(比如在AND或OR列表中)使用多條語句,你可以把它們括在花括號{}中來構造一個語句塊。例如,在本章后面給出的應用程序中,你將看到如下所示的代碼:

2.6.4 函數

你可以在shell中定義函數。如果你想編寫大型的shell腳本程序,你會想到用它們來構造自己的代碼。


作為另一種選擇,你可以把一個大型的腳本程序分成許多小一點的腳本程序,讓每個腳本完成一個小任務。但這種做法有幾個缺點:在一個腳本程序中執行另外一個腳本程序要比執行一個函數慢得多;返回執行結果變得更加困難,而且可能存在非常多的小腳本。你應該考慮自己的腳本程序中是否有可以明顯的單獨存在的最小部分,并將其作為是否應將一個大型腳本程序分解為一組小腳本的衡量尺度。


要定義一個shell函數,你只需寫出它的名字,然后是一對空括號,再把函數中的語句放在一對花括號中,如下所示:

實驗 一個簡單的函數

我們從一個非常簡單的函數開始:

運行這個腳本程序會顯示如下的輸出信息:

實驗解析

這個腳本程序還是從自己的頂部開始執行,這一點與其他腳本程序沒什么分別。但當它遇見foo( ){結構時,它知道腳本正在定義一個名為foo的函數。它會記住foo代表著一個函數并從}字符之后的位置繼續執行。當執行到單獨的行foo時,shell就知道應該去執行剛才定義的函數了。當這個函數執行完畢以后,執行過程會返回到調用foo函數的那條語句的后面繼續執行。

你必須在調用一個函數之前先對它進行定義,這有點像Pascal語言里函數必須先于調用而被定義的概念,只是在shell中不存在前向聲明。但這并不會成為什么問題,因為所有腳本程序都是從頂部開始執行,所以只要把所有函數定義都放在任何一個函數調用之前,就可以保證所有的函數在被調用之前就被定義了。

當一個函數被調用時,腳本程序的位置參數($*、$@、$#、$1、$2等)會被替換為函數的參數。這也是你讀取傳遞給函數的參數的辦法。當函數執行完畢后,這些參數會恢復為它們先前的值。

一些老版本的shell在函數執行之后可能不會恢復位置參數的值。所以如果你想讓自己的腳本程序具備可移植性,就最好不要依賴這一行為。

你可以通過return命令讓函數返回數字值。讓函數返回字符串值的常用方法是讓函數將字符串保存在一個變量中,該變量然后可以在函數結束之后被使用。此外,你還可以echo一個字符串并捕獲其結果,如下所示:

請注意,你可以使用local關鍵字在shell函數中聲明局部變量,局部變量將僅在函數的作用范圍內有效。此外,函數可以訪問全局作用范圍內的其他shell變量。如果一個局部變量和一個全局變量的名字相同,前者就會覆蓋后者,但僅限于函數的作用范圍之內。例如,你可以對上面的腳本程序進行如下的修改來查看執行結果:

如果在函數里沒有使用return命令指定一個返回值,函數返回的就是執行的最后一條命令的退出碼。

實驗 從函數中返回一個值

下一個腳本程序my_name演示了函數的參數是如何傳遞的,以及函數如何返回一個true或false值。你使用一個參數來調用該腳本程序,該參數是你想要在問題中使用的名字。

(1)在shell頭之后,我們定義了函數yes_or_no:

(2)然后是主程序部分:

這個腳本程序的典型輸出如下所示:

實驗解析

腳本程序開始執行時,函數yes_or_no被定義,但先不會執行。在if語句中,腳本程序執行到函數yes_or_no時,先把$1替換為腳本程序的第一個參數Rick,再把它作為參數傳遞給這個函數。函數將使用這些參數(它們現在被保存在$1、$2等位置參數中)并向調用者返回一個值。if結構再根據這個返回值去執行相應的語句。

如你所見,shell有著豐富的控制結構和條件語句。接下來,你需要學習一些shell的內置命令,然后你就要在不使用編譯器的情況下解決一個實際的程序設計問題了!

2.6.5 命令

你可以在shell腳本程序內部執行兩類命令。一類是可以在命令提示符中執行的“普通”命令,也稱為外部命令(external command),一類是我們前面提到的“內置”命令,也稱為內部命令(internal command)。內置命令是在shell內部實現的,它們不能作為外部程序被調用。然而,大多數的內部命令同時也提供了獨立運行的程序版本——這一需求是POSIX規范的一部分。通常情況下,命令是內部的還是外部的并不重要,只是內部命令的執行效率更高。

我們在這里將只介紹那些在編寫腳本程序時會用到的主要命令,不分內部還是外部。作為一個Linux用戶,你可能還知道許多其他可以在命令提示符下執行的合法命令。請記住,除了我們在這里介紹的內置命令外,它們同樣也可以在腳本程序中使用。

1.break命令

你可以用這個命令在控制條件未滿足之前,跳出for、while或until循環。你可以為break命令提供一個額外的數值參數來表明需要跳出的循環層數,但我們并不建議讀者這么做,因為它將大大降低程序的可讀性。在默認情況下,break只跳出一層循環。

2.:命令

冒號(:)命令是一個空命令。它偶爾會被用于簡化條件邏輯,相當于true的一個別名。由于它是內置命令,所以它運行的比true快,但它的輸出可讀性較差。

你可能會看到將它用作while循環的條件,while :實現了一個無限循環,代替了更常見的while true。

: 結構也會被用在變量的條件設置中,例如:

如果沒有:, shell將試圖把$var當作一條命令來處理。

在一些shell腳本,主要是一些舊的shell腳本中,你可能會看到冒號被用在一行的開頭來表示一個注釋。但現代的腳本總是用#來開始一個注釋行,因為這樣做執行效率更高。

3.continue命令

非常類似C語言中的同名語句,這個命令使for、while或until循環跳到下一次循環繼續執行,循環變量取循環列表中的下一個值。

continue可以帶一個可選的參數以表示希望繼續執行的循環嵌套層數,也就是說你可以部分地跳出嵌套循環。這個參數很少使用,因為它會導致腳本程序極難理解。例如:

它的輸出是:

4..命令

點(.)命令用于在當前shell中執行命令:

通常,當一個腳本執行一條外部命令或腳本程序時,它會創建一個新的環境(一個子shell),命令將在這個新環境中執行,在命令執行完畢后,這個環境被丟棄,留下退出碼返回給父shell。但外部的source命令和點命令(這兩個命令差不多是同義詞)在執行腳本程序中列出的命令時,使用的是調用該腳本程序的同一個shell。

因為在默認情況下,shell腳本程序會在一個新創建的環境中執行,所以腳本程序對環境變量所作的任何修改都會丟失。而點命令允許執行的腳本程序改變當前環境。當你要把一個腳本當作“包裹器”來為后續執行的一些其他命令設置環境時,這個命令通常就很有用。例如,如果你正同時參與幾個不同的項目,你就可能會遇到需要使用不同的參數來調用命令的情況,比如說調用一個老版本的編譯器來維護一個舊程序。

在shell腳本程序中,點命令的作用有點類似于C或C++語言里的#include指令。盡管它并沒有從字面意義上包含腳本,但它的確是在當前上下文中執行命令,所以你可以使用它將變量和函數定義結合進腳本程序。

實驗 點(.)命令

下面的例子在命令行中使用點命令,但你完全可以把它用在一個腳本程序中。

(1)假設你有兩個包含環境設置的文件,它們分別針對兩個不同的開發環境。為了設置老的、經典命令的環境,你可以使用文件classic_set,它的內容如下所示:

(2)對于新命令,使用文件latest_set:

你可以通過將這些腳本程序和點命令結合來設置環境,就像下面的示例那樣:

實驗解析

這個腳本程序使用點命令執行,所以每個腳本程序都是在當前shell中執行。這使得腳本程序可以改變當前shell中的環境設置,即使腳本程序執行結束后,這些改變仍然有效。

5.echo命令

雖然,X/Open建議在現代shell中使用printf命令,但我們還是依照常規使用echo命令來輸出結尾帶有換行符的字符串。

一個常見的問題是如何去掉換行符。遺憾的是,不同版本的UNIX對這個問題有著不同的解決方法。Linux常用的解決方法如下所示:

但你也經常會遇到:

第二種方法echo -e確保啟用了反斜線轉義字符(如\c代表去掉換行符,\t代表制表符,\n代表回車)的解釋。在老版本的bash中,對反斜線轉義字符的解釋通常都是默認啟用的,但最新版本的bash通常在默認情況下都不對反斜線轉義字符進行解釋。你所使用的Linux發行版的詳細行為請查看相關手冊。

如果你需要一種刪除結尾換行符的可移植方法,則可以使用外部命令tr來刪除它,但它執行的速度比較慢。如果你需要自己的腳本兼容UNIX系統并且需要刪除換行符,最好堅持使用printf命令。如果你的腳本只需要運行在Linux和bash上,那么echo-n是不錯的選擇,雖然你可能需要在腳本的開頭加上#! /bin/bash,以明確表示你需要bash風格的行為。

6.eval命令

eval命令允許你對參數進行求值。它是shell的內置命令,通常不會以單獨命令的形式存在。我們借用X/Open規范中的一個小例子來演示它的用法:

它輸出$foo,而

輸出10。因此,eval命令有點像一個額外的$,它給出一個變量的值的值。

eval命令十分有用,它允許代碼被隨時生成和運行。雖然它的確增加了腳本調試的復雜度,但它可以讓你完成使用其他方法難以或者根本無法完成的事情。

7.exec命令

exec命令有兩種不同的用法。它的典型用法是將當前shell替換為一個不同的程序。例如:

腳本中的這個命令會用wall命令替換當前的shell。腳本程序中exec命令后面的代碼都不會執行,因為執行這個腳本的shell已經不存在了。

exec的第二種用法是修改當前文件描述符:

這使得文件描述符3被打開以便從文件afile中讀取數據。這種用法非常少見。

8.exit n命令

exit命令使腳本程序以退出碼n結束運行。如果你在任何一個交互式shell的命令提示符中使用這個命令,它會使你退出系統。如果你允許自己的腳本程序在退出時不指定一個退出狀態,那么該腳本中最后一條被執行命令的狀態將被用作為返回值。在腳本程序中提供一個退出碼總是一個良好的習慣。

在shell腳本編程中,退出碼0表示成功,退出碼1~125是腳本程序可以使用的錯誤代碼。其余數字具有保留含義,如表2-7所示。

表2-7

用0表示成功對于許多C/C++程序員來說可能有些不尋常。在腳本程序中,這種做法的一大優點是:它使得你可以使用多達125個用戶自定義的錯誤代碼,而不需要使用一個全局的錯誤代碼變量。

下面是一個簡單的例子,如果當前目錄下存在一個名為.profile的文件,它就返回0表示成功:

如果你是個精益求精的人,或至少追求更簡潔的腳本,那么你可以組合使用前面介紹過的AND和OR列表來重寫這個腳本程序,只需要一行代碼:

9.export命令

export命令將作為它參數的變量導出到子shell中,并使之在子shell中有效。在默認情況下,在一個shell中被創建的變量在這個shell調用的下級(子)shell中是不可用的。export命令把自己的參數創建為一個環境變量,而這個環境變量可以被當前程序調用的其他腳本和程序看見。從更技術的角度來說,被導出的變量構成從該shell衍生的任何子進程的環境變量。我們用下面兩個腳本程序export1和export2來說明它的用法。

實驗 導出變量

(1)我們先列出腳本程序export2:

(2)然后是腳本程序export1。在這個腳本的結尾,我們調用了export2:

如果你運行這個腳本程序,你將得到如下的輸出:

實驗解析

export2腳本只是回顯兩個變量的值。export1腳本同時設置兩個變量,但只導出變量bar,所以當它其后調用export2時,變量foo的值已丟失,但變量bar的值已被導出到第二個腳本中。腳本輸出中第一個空行的出現是因為$foo在export2中沒有定義,回顯一個null變量將輸出一個空行。

一旦一個變量被shell導出,它就可以被該shell調用的任何腳本使用,也可以被后續依次調用的任何shell使用。如果腳本export2調用了另一個腳本,bar的值對新腳本來說仍然有效。

set -a或set -o allexport命令將導出它之后聲明的所有變量。

10.expr命令

expr命令將它的參數當作一個表達式來求值。它的最常見用法就是進行如下形式的簡單數學運算:

反引號(``)字符使x取值為命令expr $x+1的執行結果。你也可以用語法$( )替換反引號``,如下所示:

expr命令的功能十分強大,它可以完成許多表達式求值計算。表2-8列出了主要的一些求值計算。

表2-8

在較新的腳本程序中,expr命令通常被替換為更有效的$((...))語法,這個我們會在本章后面的內容中介紹。

11.printf命令

只有最新版本的shell才提供printf命令。X/Open規范建議我們應該用它來代替echo命令,以產生格式化的輸出,但看來幾乎沒什么人接受這一建議。

它的語法是:

格式字符串與C/C++中使用的非常相似,但有一些自己的限制。主要是不支持浮點數,因為shell中所有的算術運算都是按照整數來進行計算的。格式字符串由各種可打印字符、轉義序列和字符轉換限定符組成。格式字符串中除了%和\之外的所有字符都將按原樣輸出。

表2-9列出了它支持的轉義序列。

表2-9

字符轉換限定符相當復雜,所以我們在這里只列出最常見的用法。更詳細的介紹可以參考bash的在線手冊或printf在線手冊的第一部分(man 1 printf)。如果在手冊的第一部分找不到,你可以嘗試查找手冊的第三部分。字符轉換限定符由一個%和跟在后面的一個轉換字符組成。主要的轉換字符如表2-10所示。

表2-10

格式字符串然后被用來解釋printf后續參數的含義并輸出結果。例如:

注意,你必須使用雙引號括住Hi There字符串,使之成為一個單獨的參數。

12.return命令

return命令的作用是使函數返回。我們在前面介紹函數時已提到過它。return命令有一個數值參數,這個參數在調用該函數的腳本程序里被看做是該函數的返回值。如果沒有指定參數,return命令默認返回最后一條命令的退出碼。

13.set命令

set命令的作用是為shell設置參數變量。許多命令的輸出結果是以空格分隔的值,如果需要使用輸出結果中的某個域,這個命令就非常有用。

假設你想在一個shell腳本中使用當前月份的名字。系統本身提供了一個date命令,它的輸出結果中包含了字符串形式的月份名稱,但是你需要把它與其他區域分隔開。你可以將set命令和$(...)結構相結合來執行date命令,并且返回想要的結果。date命令的輸出把月份字符串作為它的第二個參數:

這個程序把date命令的輸出設置為參數列表,然后通過位置參數$2獲得月份。

注意,我們以date命令作為一個簡單的例子來說明怎么提取位置參數。由于date命令的輸出受本地語言的影響較大,所以在實際工作中,你應該使用date+%B命令來提取月份名字。date命令還有許多其他格式選項,詳細資料請參考它的手冊頁。

你還可以通過set命令和它的參數來控制shell的執行方式。其中最常用的命令格式是set -x,它讓一個腳本程序跟蹤顯示它當前執行的命令。我們將在本章后面介紹程序調試時討論set命令和它更多的選項。

14.shift命令

shift命令把所有參數變量左移一個位置,使$2變成$1, $3變成$2,以此類推。原來$1的值將被丟棄,而$0仍將保持不變。如果調用shift命令時指定了一個數值參數,則表示所有的參數將左移指定的次數。$*、$@和$#等其他變量也將根據參數變量的新安排做相應的變動。

在掃描處理腳本程序的參數時,經常要用到shift命令。如果你的腳本程序需要10個或10個以上的參數,你就需要用shift命令來訪問第十個及其后面的參數。

例如,你可以像下面這樣依次掃描所有的位置參數:

15.trap命令

trap命令用于指定在接收到信號后將要采取的行動,我們將在本書后面的內容中詳細介紹信號。trap命令的一種常見用途是在腳本程序被中斷時完成清理工作。歷史上,shell總是用數字來代表信號,但新的腳本程序應該使用信號的名字,它們定義在頭文件signal.h中,在使用信號名時需要省略SIG前綴。你可以在命令提示符下輸入命令trap -l來查看信號編號及其關聯的名稱。

對于那些不熟悉信號的人們來說,信號是指那些被異步發送到一個程序的事件。在默認情況下,它們通常會終止一個程序的運行。

trap命令有兩個參數,第一個參數是接收到指定信號時將要采取的行動,第二個參數是要處理的信號名。

請記住,腳本程序通常是以從上到下的順序解釋執行的,所以你必須在你想保護的那部分代碼之前指定trap命令。

如果要重置某個信號的處理方式到其默認值,只需將command設置為-。如果要忽略某個信號,就把command設置為空字符串’'。一個不帶參數的trap命令將列出當前設置的信號及其行動的清單。

表2-11列出了X/Open規范里面規定的能夠被捕獲的比較重要的一些信號(括號里面的數字是對應的信號編號)。更多細節請參考signal在線手冊的第7部分(man 7 signal)。

表2-11

實驗 信號處理

下面的腳本演示了一些簡單的信號處理方法:

如果你運行這個腳本,在每次循環時按下Ctrl+C組合鍵(或任何你系統上設定的中斷鍵),將得到如下所示的輸出:

實驗解析

在這個腳本程序中,我們先用trap命令安排它在出現一個INT(中斷)信號時執行rm -f/tmp/my_tmp_file_$$命令刪除臨時文件。腳本程序然后進入一個while循環,只要臨時文件存在,循環就一直持續下去。當用戶按下Ctrl+C組合鍵時,腳本程序就會執行rm -f /tmp/my_tmp_file_$$語句,然后繼續下一個循環。因為臨時文件現在已經被刪除了,所以第一個while循環將正常退出。

接下來,腳本程序再次調用trap命令,這次是指定當一個INT信號出現時不執行任何命令。腳本程序然后重新創建臨時文件并進入第二個while循環。這次當用戶按下Ctrl+C組合鍵時,沒有語句被指定執行,所以采取默認處理方式,即立即終止腳本程序。因為腳本程序被立即終止了,所以最后的echo和exit語句永遠都不會被執行。

16.unset命令

unset命令的作用是從環境中刪除變量或函數。這個命令不能刪除shell本身定義的只讀變量(如IFS)。這個命令并不常用。

下面的腳本第一次輸出字符串Hello World,但第二次只輸出一個換行符:

使用foo=語句產生的效果與上面腳本中的unset命令產生的效果差不多,但并不等同。foo=語句將變量foo設置為空,但變量foo仍然存在,而使用unset foo語句的效果是把變量foo從環境中刪除。

17.另外兩個有用的命令和正則表達式

在學習如何應用shell編程中的這個新知識點之前,讓我們再來看另外兩個非常有用的命令,它們雖然不是shell的一部分,但在編寫shell程序時經常用到。同時,我們也將介紹正則表達式,一種出現在所有Linux以及與之關聯程序中的模式匹配特征。

find命令

你將看到的第一個命令是find。這是個用于搜索文件的命令,它極其有用,但Linux初學者常常覺得它不易使用,這不僅僅是因為它有選項、測試和動作類型的參數,還因為其中一個參數的處理結果可能會影響到后續參數的處理。

在深入研究這些選項、測試和參數之前,讓我們首先看一個非常簡單的例子,它用來在本地機器上查找名為test的文件。為了確保你具有搜索整個機器的權限,請以root用戶身份來執行這個命令:

根據你所使用系統的不同,你可能還會找到其他幾個名稱也為test的文件。正如你可能猜測的那樣,這個命令的含義是:從根目錄開始查找名為test的文件,并且輸出該文件的完整路徑。這非常簡單。

然而,這個命令的執行確實需要花費很長的時間,并且網絡上的Windows機器的硬盤也會高速轉動。這是因為Linux機器掛載(使用SAMBA)了一大塊Windows機器的文件系統,看起來似乎是Windows文件系統也被搜索了,盡管我們知道要查找的文件應該在Linux機器上。

這就是我們要介紹的第一個選項發揮作用的時候了。如果你指定-mount選項,你就可以告訴find命令不要搜索掛載的其他文件系統的目錄。

我們仍然能找到文件,但這次搜索速度會更快,同時也不必再搜索掛載的其他文件系統。

find命令的完整語法格式如下所示:

path部分很容易理解:你既可以使用絕對路徑,如/bin,也可以使用相對路徑,如.。如果需要,你也可以指定多個路徑,如find /var /home。

find命令有許多選項可用,表2-12列出了一些主要的選項。

表2-12

下面是測試部分。可以提供給find命令的測試非常多,每種測試返回的結果有兩種可能:true或false。find命令開始工作時,它按照順序將定義的每種測試依次應用到它搜索到的每個文件上。如果一個測試返回false, find命令就停止處理它當前找到的這個文件,并繼續搜索。如果一個測試返回true, find命令將繼續下一個測試或對當前文件采取行動。表2-13只列出了最常用的測試,請參考find命令的手冊頁以了解所有可以使用的測試。

表2-13

你還可以用操作符來組合測試。大多數操作符有兩種格式:短格式和長格式,如表2-14所示。

表2-14

你可以通過使用圓括號來強制測試和操作符的優先級。由于圓括號對shell來說有其特殊的含義,所以你還必須使用反斜線來引用圓括號。此外,如果你在文件名處使用的是匹配模式,你就必須在模式上使用引號以確保模式沒有被shell擴展,而是直接傳遞給find命令。例如,如果你想寫一個測試“搜索的文件比文件X要新,或者文件名以下劃線開頭”,你可以這樣寫:

我們將在下一個“實驗解析”部分之后舉這樣一個例子。

實驗 使用帶測試的find命令

在當前目錄下搜索比文件while2要新的文件:

這個結果看起來不錯,不過在結果中還包括了當前目錄,而這并不是你想要的,你只對普通文件感興趣。所以你會增加一個額外的測試-type f:

實驗解析

它是如何工作的呢?你指定find命令應該在當前目錄(.)中搜索比文件while2要新的文件(-newer while2),如果這個測試通過,然后再測試這個文件是否是一個普通文件(-type f)。最后,你使用前面已經講過的-print來確認搜索到的文件。

現在來查找以下劃線開頭的文件或比while2文件要新的文件,但在兩種情況下都必須是普通文件。這個例子將演示如何使用圓括號來對測試進行組合:

可以看出完成這個任務并不是很困難。你必須轉義圓括號使得它們不會被shell處理,而且還需要將*號用引號括起使得它被直接傳遞給find命令。

現在你已可以可靠地搜索文件了。下面來看看在發現匹配指定條件的文件之后,你可以執行的動作。表2-15只列出了最常見的動作,完整的動作列表請見find命令的手冊頁。

-exec和-ok命令將命令行上后續的參數作為它們參數的一部分,直到被\;序列終止。實際上,-exec和-ok命令執行的是一個嵌入式命令,所以嵌入式命令必須以一個轉義的分號結束,使得find命令可以決定什么時候它可以繼續查找用于它自己的命令行選項。魔術字符串{}是-exec或-ok命令的一個特殊類型的參數,它將被當前文件的完整路徑取代。

表2-15

上面的解釋可能并不容易理解,但通過一個例子可以將其解釋得更清楚。我們來看一個比較簡單的例子,它使用一條非常安全的命令ls:

如你所見,find命令非常有用。你只需通過一點練習就可以很好地掌握它。無論如何,這點練習是完全值得的,所以請使用find命令來進行實驗。

grep命令

我們將介紹的第二個非常有用的命令是grep,這個不尋常的名字代表的是通用正則表達式解析器(General Regular Expression Parser,簡寫為grep)。你使用find命令在系統中搜索文件,而使用grep命令在文件中搜索字符串。事實上,一種非常常見的用法是在使用find命令時,將grep作為傳遞給-exec的一條命令。

grep命令使用一個選項、一個要匹配的模式和要搜索的文件,它的語法如下所示:

如果沒有提供文件名,則grep命令將搜索標準輸入。

我們首先來查看grep命令的一些主要選項,它們列在了表2-16中,完整的選項列表請見grep命令的手冊頁。

表2-16

實驗 基本的grep命令用法

我們來看一些使用grep命令的簡單例子:

實驗解析

第一個例子未使用選項,它只是在文件words.txt中搜索字符串in,然后輸出匹配的行。文件名未輸出是因為你只在一個文件中進行搜索。

第二個例子在兩個不同的文件中計算匹配行的數目。在這種情況下,文件名被輸出。

最后一個例子使用-v選項對搜索取反,在兩個文件中計算不匹配行的數目。

正則表達式

正如你所看到的,grep命令的基本用法非常容易掌握。現在是時候介紹正則表達式的基礎知識了,它允許你實現更復雜的匹配。正如我們在本章前面提到的那樣,正則表達式被廣泛應用于Linux和許多其他開源編程語言中。你可以在vi編輯器或Perl腳本中使用它們,而且不論它們出現在哪里,其基本原理都是一樣的。

在正則表達式的使用過程中,一些字符是以特定方式處理的。最常使用的特殊字符如表2-17所示。

表2-17

如果想將上述字符用作普通字符,就需要在它們前面加上\字符。例如,如果想使用$字符,你需要將它寫為\$。

在方括號中還可以使用一些有用的特殊匹配模式,如表2-18所示。

表2-18

另外,如果指定了用于擴展匹配的-E選項,那些用于控制匹配完成的其他字符可能會遵循正則表達式的規則(見表2-19)。對于grep命令來說,我們還需要在這些字符之前加上\字符。

表2-19

這看上去有點復雜,但如果你實際應用它,將會發現它并不像第一眼看上去那么復雜。掌握正則表達式的最簡單方法就是嘗試一些實驗。

(1)我們的第一個例子是查找以字母e結尾的行。你可能會猜到需要使用特殊字符$,如下所示:

如你所見,這個命令找到了以字母e結尾的行。

(2)現在假設想要查找以字母a結尾的單詞。要完成這一任務,你需要使用方括號括起的特殊匹配字符。在本例中,你將使用的是[[:blank:]],它用來測試空格或制表符:

(3)下面我們來查找以Th開頭的由3個字母組成的單詞。在本例中,你既需要使用[[:space:]]來劃定單詞的結尾,還需要用字符(.)來匹配一個額外的字符:

(4)最后,我們用擴展grep模式來搜索只有10個字符長的全部由小寫字母組成的單詞。我們通過指定一個匹配字母a到z的字符范圍和一個重復10次的匹配來完成這一任務:

我們在這里只涉及了正則表達式中最重要的內容。與Linux中大多數事物一樣,系統中的大量文檔可以幫助你了解更多的細節,但學習正則表達式最好的方法是實際操作。

2.6.6 命令的執行

編寫腳本程序時,你經常需要捕獲一條命令的執行結果,并把它用在shell腳本程序中。也就是說,你想要執行一條命令,并把該命令的輸出放到一個變量中。

你可以用在本章前面set命令示例中介紹的$(command)語法來實現,也可以用一種比較老的語法形式`command`,這種用法目前依然很常見。

請注意,在腳本程序里執行命令的比較老的語法形式時,使用的是反引號(`),而不是我們在前面使用的單引號(')(單引號的作用是防止變量擴展)。只有當你需要使自己的腳本程序具備非常好的可移植性時,你才應該使用這種比較老的方法。

所有的新腳本程序都應該使用$(...)形式,引入這一形式的目的是為了避免在使用反引號執行命令時,處理其內部的$、`、\等字符所需要應用的相當復雜的規則。如果在反引號`...`結構中需要用到反引號,它就必須通過\字符進行轉義。這些相對晦澀的字符往往會讓程序員感到困惑,有時即使是經驗豐富的shell腳本程序員也必須反復進行實驗,才能確保在反引號命令中引號的使用不會出錯。

$(command)的結果就是其中命令的輸出。注意,這不是該命令的退出狀態,而是它的字符串形式的輸出結果。例如:

因為當前目錄是一個shell環境變量,所以程序的第一行不需要使用這個命令執行結構。但如果我們想要在腳本程序中使用who命令的輸出結果,就需要使用這個結構。

如果想要將命令的結果放到一個變量中,你可以按通常的方法來給它賦值,如下所示:

這種把命令的執行結果放到變量中的能力是非常強大的,它使得在腳本程序中使用現有命令并捕獲其輸出變得很容易。如果需要把一條命令在標準輸出上的輸出轉換為一組參數,并且將它們用做為另一個程序的參數,你會發現命令xargs可以幫你完成這一工作。具體細節請參考它的手冊頁。

有時,當你打算調用的命令在輸出你想要的內容之前先輸出了一些空白字符,或者它輸出的內容比你想要的要多的時候也會出現問題。此時,你可以用前面介紹的set命令來解決。

1.算術擴展

我們已經介紹過expr命令,通過它可以處理一些簡單的算術命令,但這個命令執行起來相當慢,因為它需要調用一個新的shell來處理expr命令。

一種更新更好的辦法是使用$((...))擴展。把你準備求值的表達式括在$((...))中能夠更有效地完成簡單的算術運算。如下所示:

注意,這與x=$(...)命令不同,兩對圓括號用于算術替換,而我們之前見到的一對圓括號用于命令的執行和獲取輸出。

2.參數擴展

你已經見過形式最簡單的參數賦值和擴展了,如下所示:

但當你想在變量名后附加額外的字符時就會遇到問題。假設你想編寫一個簡短的腳本程序,來處理名為1_tmp和2_tmp的兩個文件。你可能會這樣寫:

但是在每次循環中,你都會看到如下所示的出錯信息:

哪里出錯了呢?

問題在于shell試圖替換變量$i_tmp的值,而這個變量其實并不存在。shell并不會認為這是一個錯誤,僅僅會將它替換為一個空值,因此根本不會有參數被傳遞給my_secret_process。為了保護變量名中類似于$i部分的擴展,你需要把i放在花括號中,如下所示:

在每次循環中,變量i的值替換了${i},從而給出正確的文件名。也就是說,你把參數的值替換進了一個字符串。

你可以在shell中采用多種參數替換方法。對于多參數處理問題來說,這些方法通常會提供一種精巧的解決方案。表2-20列出了一些常見的參數擴展方法。

表2-20

當處理字符串時,這些替換通常是很有用的。特別是上表中對字符串進行部分刪除的最后4個參數擴展方法,在處理文件名和路徑時非常有用,請看下面的例子。

實驗 參數的處理

下面腳本程序的各個部分分別演示了各種參數匹配操作符的用法:

它的輸出結果如下:

實驗解析

第一條語句${foo:-bar}給出的值是bar,這是因為在這條語句執行時foo沒有值。這條語句執行后,變量foo未發生變化,它還停留在未設置狀態。

如果這條語句是${foo:=bar},那么變量$foo就會被賦值。這個字符串操作符的作用是,檢查變量foo是否存在且不為空。如果它不為空,就返回它的值,否則就把變量foo賦值為bar并返回這個值。

${foo:? bar}語句將在變量foo不存在或它設置為空的情況下,輸出foo:bar并異常終止腳本程序。最后,${foo:+bar}語句將在變量foo存在且不為空的情況下返回bar。選擇可太多了!

{foo#*/}語句僅僅匹配并刪除最左邊的/(記住,*匹配零個或多個字符)。{foo##*/}語句匹配并刪除盡可能多的字符,所以它刪除最右邊的/及其前面的所有字符。

{bar%local*}語句匹配從右邊起直到第一次出現local(及跟在它后面的所有字符),而{bar%%local*}語句則從右邊起盡可能多地匹配字符,直到遇到最靠左邊的local。

因為UNIX和Linux系統都非常依賴過濾器的思想,所以一個操作的結果常常必須手工進行重定向。假設你想使用cjpeg程序將一個GIF文件轉換為一個JPEG文件:

但有時,你可能希望對大量文件執行這類操作,那么如何實現自動重定向呢?很簡單,像下面這樣做即可:

這個腳本名為giftojpeg,它為當前目錄中的每個GIF文件創建一個對應的JPEG文件。

2.6.7 here文檔

在shell腳本程序中向一條命令傳遞輸入的一種特殊方法是使用here文檔。它允許一條命令在獲得輸入數據時就好像是在讀取一個文件或鍵盤一樣,而實際上是從腳本程序中得到輸入數據。

here文檔以兩個連續的小于號<<開始,緊跟著一個特殊的字符序列,該序列將在文檔的結尾處再次出現。<<是shell的標簽重定向符,在這里,它強制命令的輸入是一個here文檔。這個特殊字符序列的作用就像一個標記,它告訴shell here文檔結束的位置。因為這個標記序列不能出現在傳遞給命令的文檔內容中,所以應該盡量使它既容易記憶又相當不尋常。

實驗 使用here文檔

最簡單的例子就是給cat命令提供輸入數據,如下所示:

它的輸出如下所示:

here文檔功能可能看起來相當奇怪,但其實它的作用很大。因為它可以用來調用交互式的程序,比如一個編輯器,并向它提供一些事先定義好的輸入。但它更常見的用途是在腳本程序中輸出大量的文本,就像你在剛才的示例中看到的那樣,從而可以避免用echo語句來輸出每一行。你可以在標識符兩端都使用感嘆號(!)來確保不會引起混淆。

如果想按預定的方式處理一個文件中的幾行,你可以使用ed行編輯器,并在腳本程序中通過here文檔向它提供命令。

實驗here文檔的另一個用法

(1)我們從名為a_text_file的文件開始,它的內容如下所示:

(2)你可以通過結合使用here文檔和ed編輯器來編輯這個文件:

運行這個腳本程序,現在這個文件的內容是:

實驗解析

這個腳本程序只是調用ed編輯器并向它傳遞命令,先讓它移動到第三行,然后刪除該行,再把當前行(因為第三行剛剛被刪除了,所以當前行現在就是原來的最后一行,即第四行)中的is替換為was。完成這些操作的ed命令來自腳本程序中的here文檔——在標記!FunkyStuff!之間的那些內容。

注意,我們在here文檔中用\字符來防止$字符被shell擴展。\字符的作用是對$進行轉義,讓shell知道不要嘗試把$s/is/was/擴展為它的值,而它也確實沒有值。shell把\$傳遞為$,再由ed編輯器對它進行解釋。

2.6.8 調試腳本程序

腳本程序的調試通常都很容易,但并沒有特定的工具幫助我們進行調試。在本節中,我們將簡單講述一些常用的方法。

出現錯誤時,shell一般都會打印出包含錯誤的行的行號。如果這個錯誤并不是非常明顯,你可以添加一些額外的echo語句來顯示變量的內容,也可以通過在shell中交互式地輸入代碼片段來對它們進行測試。

因為腳本程序是解釋執行的,所以在腳本程序的修改和重試過程中沒有編譯方面的額外開支。跟蹤腳本程序中復雜錯誤的主要方法是設置各種shell選項。為此,你可以在調用shell時加上命令行選項,或是使用set命令。表2-21列出了各種選項。

表2-21

你可以用-o選項啟用set命令的選項標志,用+o選項取消設置,對簡寫版本也是一樣的處理方法。你可以通過使用xtrace選項來得到一份簡單的執行跟蹤報告。在調試的初始階段,你可以先使用命令行選項的方法,但如果想獲得更好的調試效果,你可以將xtrace標志(用來啟用或關閉執行命令的跟蹤)放到腳本程序中問題代碼的前后。執行跟蹤功能讓shell在執行每行語句之前,先輸出該行并對該行中變量進行擴展。

使用下面的命令來啟用xtrace選項:

再用下面的命令來關閉xtrace選項:

默認情況下,變量擴展的層次由每行代碼前的+號個數指出。你可以通過對shell配置文件中的shell變量PS4進行設置,將+號修改為更有意義的字符。

在shell中,你還可以通過捕獲EXIT信號,從而在腳本程序退出時查看到程序的狀態。具體做法是在腳本程序的開始處添加類似下面這樣的一條語句:

主站蜘蛛池模板: 定安县| 汉沽区| 枣庄市| 婺源县| 肥东县| 巴南区| 达州市| 类乌齐县| 大连市| 嵊州市| 金乡县| 罗平县| 新闻| 万载县| 四川省| 祁连县| 乌审旗| 天镇县| 阿荣旗| 平罗县| 彭州市| 新乡市| 长垣县| 肥乡县| 普洱| 贺兰县| 临高县| 准格尔旗| 上高县| 白银市| 沂南县| 华亭县| 盘锦市| 岳池县| 拜城县| 湘阴县| 海门市| 柳江县| 张家口市| 应城市| 青海省|