天天看點

《UNIX/Linux 系統管理技術手冊(第四版)》——2.2 bash腳本程式設計

本節書摘來自異步社群《unix/linux 系統管理技術手冊(第四版)》一書中的第2章,第2.2節,作者:【美】evi nemeth , garth snyder , trent r.hein , ben whaley著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

unix/linux 系統管理技術手冊(第四版)

bash特别适合編寫簡單的腳本,用來自動執行那些以往在指令行輸入的操作。在指令行用的技巧也能用在bash的腳本裡,反之亦然,這讓使用者在bash上投入的學習時間獲得了最大的回報。不過,一旦bash腳本超過了100行,或者需要的特性bash沒有,那麼就要換到perl或者python上了。

bash腳本的注釋以一個井号(#)開頭,并且注釋一直延續到行尾。和指令行中一樣,可以把邏輯上的一行分成多個實體上的多行來寫,每行末尾用反斜線消除換行符(newline)。還可以用分号分隔語句的辦法,在一行裡書寫多條語句。

bash腳本可以隻包含一系列的指令行,此外其他什麼都沒有。例如,下面的helloworld腳本就隻有一條echo指令。

第一行叫做“#!”語句,它聲明這個文本檔案是一個腳本,要由/bin/bash來解釋。核心在決定如何執行這個檔案的時候,要先找這個語句。從派生出來執行這個腳本的shell的角度來看,“#!”行隻是一個注釋行。如果bash不在這行指定的位置那裡,那麼就需要調整這行的内容。

要讓這個檔案做好能運作的準備,隻要設定它的可執行位即可(參考6.5.5節)。

1

還可以把shell當做解釋程式直接調用:

第一條指令在一個shell的新執行個體中運作helloworld腳本,第二條指令讓目前的登入shell讀取并執行這個檔案的内容。當這個腳本用來設定環境變量,或者隻對目前的shell做定制的時候,就采用後一種選擇。在腳本程式設計中,這種形式常用來加入一個配置檔案的内容,該檔案裡面寫的是對一系列bash變量進行指派2。

如果是windows使用者,那麼可能已經習慣于這樣的做法,即由檔案的擴充名标明該檔案的類型,以及是否可以執行。但在unix和linux上,要由檔案的權限位來指定一個檔案是否可以執行,如果可執行,那麼由誰可以執行。如果願意,可以給自己的bash腳本加.sh字尾,提醒使用者它們是什麼檔案,但在運作該指令的時候,就必須得輸入.sh,因為unix不會對擴充名做特殊處理。參考6.5.1節了解有關權限位的更多知識。

2.2.1 從指令到腳本

在我們開始介紹bash的腳本程式設計特性之前,先講一下方法。大多數人寫bash腳本的時候,都按照和他們寫perl或者python腳本一樣的方式:用一個文本編輯器來寫。不過,把正常的shell指令行當做一種互動式的腳本開發環境,考慮這樣用的話,效果會更高。

例如,假定在一個目錄層次結構中,散布着很多日志檔案,它們的名字字尾為.log和.log,現在想把它們都改為大寫的形式。首先,讓我們看看是否能找到所有這樣的檔案。

哦,看起來我們要在搜尋模式中包括點号(.),而且還要排除目錄。鍵入重新找回這條find指令,然後對它進行修改。

好了,這次看上去結果更好了。不過,.do-not-touch目錄看上去挺危險的;我們或許不應該讓它出來搗亂。

好了,正好剩下需要的檔案清單。讓我們生成一些新的名字。

好,那幾條指令就是我們想要的指令,把它們運作起來就可以執行改名操作。那麼在現實中,我們該怎麼做呢?我們可以把這條指令重新找回來,編輯一下把echo去掉,讓bash執行mv指令,而不僅僅是列印mv指令。不過,用管道把這些指令都送到另一個shell的執行個體,這樣更不容易出錯,而且需要對前面指令做的編輯也更少。

當鍵入的時候,我們會發現bash考慮得很精心,它把這個小小的腳本變成了一行。對于這個緊湊的指令行,我們隻要加一個管道,把輸出送給bash -x就行了。

給bash加了-x選項後,它在執行每條指令之前,會先列印這條指令。

我們現在已經完成了實際的改名工作,但是仍然想把整個腳本儲存下來,以便可以再次使用它。bash的内置指令fc和非常像,但它不是讓上次的指令重新出現在指令行,而是把該指令送到使用者選擇的編輯器裡。再加一個“#!”行和用法說明之後,把這個檔案寫到一個可以執行的地方(或許是~/bin,或者/usr/local/bin),讓這個檔案可執行,于是就得到了最終的腳本。

上述方法總結如下:

按一個管道的方式開發腳本(或者腳本的組成部分),一次開發一步,完全都在指令行上做;

把輸出送到标準輸出,檢查并確定結果正确;

每開發一步,用shell的history指令重新找回指令管道,用shell的編輯功能調整它們;

在得到正确輸出之前,都不實際執行任何操作,是以如果指令不正确,也不需要撤銷什麼操作;

一旦得到正确的輸出,就真正執行指令,并核對指令能按預期要求工作;

用fc指令捕獲工作結果,整理後儲存下來。

在上面的例子裡,我們列印出數行指令,然後用管道把它們送入一個子shell去執行。這一技術并不一定行得通,但它經常還是有幫助的。另一種做法是,可以把輸出重定向到一個檔案,得到這個結果。無論怎樣,都要預先看到正确的結果,才做任何可能有破壞性的操作。

2.2.2 輸入和輸出

echo指令雖然原始,但易于使用。要想對輸出做更多的控制,就需要使用printf指令。因為采用printf的話,必須顯式地在必要的地方加換行符(用“n”),是以它用起來稍有不便,不過它也能讓使用者使用制表符,而且能讓輸出裡的數字有更好的格式。比較下面兩條指令的輸出。

有些系統帶有作業系統級的echo和printf指令,通常分别位于/bin和/usr/bin目錄下。雖然這兩條指令和shell的内置指令都很相似,但是它們的細節還是稍有不同,特别是printf,差别更大一些。對此,要麼堅持采用bash的文法,要麼用完整路徑名調用外部的printf指令。

用read指令可以提示輸入。下面是一個例子:

echo指令的-n選項消除了通常的換行符,但也可以在這裡用printf指令。我們簡要介紹一下if語句的文法,它的作用在這裡很明顯。if語句裡的-n判斷其字元串參數是否為空,不為空的話則傳回真(true)。下面是這個腳本運作後的結果:

<code>$ sh readexample enter your name: ron hello ron!</code>

2.2.3 指令行參數和函數

給一個腳本的指令行參數可以成為變量,這些變量的名字是數字。$1是第一個指令行參數,$2是第二個,以此類推。$0是調用該腳本所采用的名字。這個名字可以是像../bin/example.sh這樣的奇怪名字,是以它的取值并不固定。

變量$#是提供給腳本的指令行參數的個數,變量$*裡儲存有全部參數。這兩個變量都不包括或者算上$0。

如果調用的腳本不帶參數,或者參數不正确,那麼該腳本應該列印一段用法說明,提醒使用者怎樣使用它。下面這個腳本的例子接受兩個參數,驗證這兩個參數都是目錄,然後顯示它們。如果參數無效,那麼這個腳本會列印一則用法說明,并且用一個非零的傳回碼退出。如果調用這個腳本的程式檢查該傳回碼,那麼它就會知道這個腳本沒有正确執行。

我們建立了一個單獨的show_usage函數,用它列印用法說明。如果這個腳本以後又做了更新,能夠接受更多的參數,那麼隻要在一個地方修改用法說明就行了3。

bash函數的參數就按指令行參數那樣處理。第一個參數變成$1,以此類推。正如上面的例子所示,$0是這個腳本的名字。

要讓上面的例子更健壯一點兒,我們可以編寫show_usage函數,讓它接受一個出錯碼作為參數。對于執行不成功的每一種不同類型,傳回一個定義好的出錯碼。下面的代碼片段給出了該函數的樣子。

下面這個版本的函數,其參數可有可無。在一個函數内部,$#表明傳入了多少個參數。如果沒有提供更确定的出錯碼,那麼這個腳本就傳回代碼99。但是如果給這個函數一個确定的出錯碼值,就會讓腳本在列印用法說明之後以那個出錯碼退出,例如:

(shell變量$?是上次執行的指令退出的狀态,而且無論該指令是在一個腳本内部使用,還是在指令行上使用。)

在bash裡,函數和指令之間很類似。使用者可以在自己的~/.bash_profile檔案裡定義自己的函數,然後在指令行上使用它們,就好像它們是指令一樣。例如,如果站點裡統一将網絡端口7988用于ssh協定(“不公開,即安全”的一種形式),就可以在~/.bash_profile檔案裡定義

以保證ssh總是帶選項-p7988來運作。和許多shell一樣,bash也有一種别名機制,能更加簡潔地再現上面這個限制端口的例子,不過采用函數的方法更通用,功能也更強。忘掉别名,采用函數吧。

2.2.4 變量的作用域

在腳本裡的變量是全局變量,但是函數可以用local聲明語句,建立自己的局部變量。考慮下面的代碼:

下面的日志顯示在localizer函數内,局部變量$a屏蔽了全局變量$a。在localizer内,在碰到local聲明了局部變量$a之前,全局變量$a都可見;local實際上是一條指令,它從執行的那個地方開始,建立局部變量。

2.2.5 控制流程

我們在本章裡已經見過幾種if-then和if-then-else語句的形式;它們的功能在其名字中得以展現。一條if語句的結束辨別是fi。要把幾條if語句串起來,可以用elif這個關鍵字,它的意思是“else if”。例如:

用[]做比較的奇特文法,以及整數比較運算符的名字(例如,-eq),都看上去像是指令行選項,它們二者都是從原來bourne shell的/bin/test指令延續下來的。方括号實際上是調用test的一種快捷方式,而不是if語句的文法要求4。

表2.2給出了bash的數值和字元串比較運算符。bash比較數值采用文字運算符,而比較字元串采用符号運算符,這正好和perl相反。

《UNIX/Linux 系統管理技術手冊(第四版)》——2.2 bash腳本程式設計

bash對檔案屬性取值的那些選項是其出彩之處(還是其/bin/test遺留下來的特性)。bash有大量的測試檔案和比較檔案的運算符,表2.3列出了其中幾個。

《UNIX/Linux 系統管理技術手冊(第四版)》——2.2 bash腳本程式設計

雖然elif的形式能用,但是為了清楚起見,用case語句做選擇是更好的方法。case的文法如下面的這個函數例程所示,該函數集中給一個腳本寫日志。特别值得注意的是,每一選擇條件之後有個右括号,而在條件符合時每個要執行的語句塊之後有兩個分号。case語句以esac結尾。

這個函數示範了許多系統管理應用經常采取的“日志級别”方案。腳本的代碼産生詳盡程度不同的日志消息,但是隻有那些在全局設定的門檻值$log_level之内的消息才被真正記錄到日志裡,或者采取相應的行動。為了闡明每則消息的重要性,在消息文字之前用一個标簽說明其關聯的日志級别。

2.2.6 循環

bash的for…in結構可以讓它很容易對一組值或者檔案執行若幹操作,尤其是和檔案名通配功能(對諸如和?這樣的模式比對字元進行擴充,形成檔案名或者檔案名的清單)聯合起來使用的時候。在下面這個for循環裡,其中的.sh模式會傳回目前目錄下能夠比對的檔案名清單。for語句則逐一周遊這個清單,接着把每個檔案名指派給變量$script。

輸出結果如下:

在這裡的上下文關系中,對檔案名做擴充并沒有什麼玄妙之處;它的做法就和在指令行上一模一樣。也就是說,先擴充,然後再由解釋器處理已經擴充過的這一行5。也可以靜态地輸入檔案名,就像下面這行一樣。

實際上,任何以空白分隔的對象清單,包括一個變量的内容,都可以充當for …in語句的目标體。

bash也有從傳統程式設計語言看來更為熟悉的for循環,在這種for循環裡,可以指定起始、增量和終止子句。例如:

接下來的例子示範了bash的while循環,這種循環也能用于處理指令行參數,以及讀取一個檔案裡的各行。

下面是輸出結果:

這個腳本片段有兩個有趣的功能。exec語句重新定義了該腳本的标準輸入,變成由第一個指令行參數指定的任何檔案6。這個檔案必須要有,否則腳本就會出錯。

在while子句裡的read語句實際上是shell的内置指令,但它的作用就和一條外部指令一樣。外部指令也可以放在while子句裡;在這種形式下,當外部指令傳回一個非零的退出狀态時,就會結束while循環。

表達式$((counter++))實際上是個醜小鴨。$((…))這樣的寫法要求強制進行數值計算。它還可以利用$來标記變量名。++是人們在c和其他語言中熟悉的後置遞增運算符。它傳回它前面的那個變量的值,但傳回之後還要把這個變量的值再加1。

$((…))的技巧在雙引号裡也起作用,是以可以把整個循環體緊湊地寫到一行裡。

2.2.7 數組和算術運算

複雜的資料結構和計算不是bash的特長。但它的确至少提供了數組和算術運算。

所有bash變量的值都是字元串,是以bash在指派的時候并不區分數字1和字元串“1”。不同之處在于如何使用變量。下面幾行代碼展示出了其中的差異:

這個腳本産生的輸出如下:

注意給$c指派的語句,其中的加号(+)連字元串的連接配接運作符都不是。它僅僅就是一個字元而已。那行代碼等價于

為了強制進行數值計算,要把這個表達式放在$((…))裡面,就像上面給$d指派那樣。但即便如此,也不會讓$d獲得一個數值;它的值仍然儲存為字元串“3”。

bash通常能夠混合使用算術、邏輯和關系運算符;參考手冊頁了解詳情。

bash中的數組有點兒怪,是以不常用到它們。然而,如果需要它們也依然可以用。數組用括号括起來,數組元素之間用空白隔開。數組元素中的空白要用引号引起來。

單個數組元素用${array name [subscript]}來通路。下标從0開始。下标和@指整個數組,${#array name[]}和${#array name[@]}這兩種特殊形式表示數組裡元素的個數。不要把它們和似乎更合乎邏輯的${#array name}搞混了;後者實際上是數組第一個元素的長度(等價于${#array name[0]})。

讀者可能會以為$example[1]是指數組的第二個元素,這一點無可争議,但bash對這個字元串的分析結果卻是:$example(即$example[0]的簡潔引用形式)加上一個字元串[1]。在通路數組變量的時候,一定要帶花括号——這一點無一例外。

下面是一個快速腳本,它示範了bash中數組管理的一些功能和缺陷:

這個腳本的輸出如下:

這個例子似乎很直覺易懂,但隻是因為我們已經把這個腳本構造得循規蹈矩了。人們一不小心就會犯錯誤。例如,用下面這一句替換for語句那行代碼:

(在數組表達式外面沒有用引号引起來)也能行,但它卻不是輸出4個數組元素,而是5個:aa、bb、cc、dd和ee。

這背後的問題是,因為所有bash變量實質上仍然是字元串,是以數組的表象充其量還是不确定的。字元串什麼時候分割成數字元素,怎樣分割成數組元素,都有很多細微變化。讀者可以使用perl或者python,或是用谷歌搜尋mendel cooper的advanced bash-scripting guide來研究這些細微差别。