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

好的系統管理者都要寫腳本。腳本以标準和自動的方式履行系統管理者的繁雜事務,藉此把管理者的時間節省出來,以花在更重要和更有意思的任務上。從某種意義上講,腳本也是一種低品質的文檔,因為它們充當了一種權威提綱,提綱裡列出完成特殊任務所需的步驟。
從複雜性來看,系統管理腳本的範圍很廣,小到一個腳本,簡單得隻封裝幾條靜态指令,大到一個重要的軟體項目,為整個站點管理主機配置和管理性資料。在本書裡,我們所感興趣的主要是系統管理者通常會碰到的較小的日常腳本項目。是以,對于較大項目才需要的支援功能(例如,bug追蹤和設計評審),我們不會講得太多。
系統管理腳本應該注重兩點,即程式設計人員的開發效率和代碼的清晰可讀性。計算效率不應該成為關注重點,但這不應成為草率行事的借口,而是要認識到,很少需要在意一個腳本是在半秒還是兩秒内運作完。優化腳本獲得的回報都非常低,甚至對通過cron定期運作的腳本來說也不例外。
長期以來,編寫系統管理腳本的标準語言都是shell所定義的語言。在大多數系統上,預設的shell都是bash(即“bourne again” shell),但是在幾種不多的unix系統上,也用sh(最初的bourne shell)和ksh(korn shell)。shell腳本一般用于輕量級的任務,如自動執行一系列指令,或者把幾個過濾器組合起來處理資料。
各種作業系統上都有shell,是以shell腳本的可移植性相當好,除了它們調用的指令之外,要依賴的東西也不多。無論是否選擇shell來編寫腳本,都會碰到shell:大多數環境都包括對已有sh腳本的強大補充,系統管理者會頻頻閱讀、了解和調整這些腳本。
對于更為複雜高端的腳本來說,建議轉而采用一種真正的程式設計語言來寫,像perl或者python這樣的語言,它們兩者都很适合于系統管理工作。這兩種語言融入的設計理念比shell領先20年,它們的字處理功能(對于系統管理者來說,價值難以估量)如此強大,sh在它們面前黯然失色。
perl和python的主要缺點在于,建立它們的環境要麻煩一點兒,尤其是要用到的第三方庫,而庫裡又包含已經編譯好的部件的時候。shell沒有子產品結構,也沒有第三方的庫,是以避開了這個特殊的問題。
本章簡要介紹了bash、perl和python作為腳本程式設計語言的用法,以及正規表達式這種通用的技術。
unix/linux 系統管理技術手冊(第四版)
在我們開始介紹shell的腳本程式設計之前,讓我們先看看shell的一些基本特性和文法。不管讀者正在使用的是何種平台,本節都适用于sh大家庭裡的所有主流shell(包括bash和ksh,但不包括csh或者tcsh)。嘗試一下自己不熟悉的sh形式,做做實驗吧!
2.1.1 編輯指令
我們已經注意到一點,太多人都用方向鍵來編輯指令行。但讀者朋友不會在文字編輯器裡這麼做,對嗎?
如果喜歡emacs,那麼在編輯指令曆史的時候,所有的emacs基本指令都用得上。用到行尾,用到指令行的開頭。用一條條回退到最近執行過的指令,重新把它們調出來進行編輯。用增量搜尋指令曆史找出老指令。
如果喜歡vi,那麼用下面的指令就能讓shell的指令行編輯進入vi模式:
和在vi裡一樣,編輯操作是有模式的;不過,一開始會進入輸入模式。按鍵離開輸入模式,按“i”鍵重新進入輸入模式。在編輯模式下,“w”鍵向前進一個單詞,“fx”在本行裡找到下一個x,等等。用 k[譯者注:即同時按下鍵和k鍵]可以周遊過去輸入的指令。想要再次回到emacs編輯模式嗎,可使用下面的指令實作
2.1.2 管道和重定向
每個程序都至少有3個信道:“标準輸入”(stdin)、“标準輸出”(stdout)和“标準出錯”(stderr)。核心給每個程序都設定了這3個信道,是以程序本身不必知道這三個信道通到哪裡。舉例來說,它們可能連接配接到一個終端視窗、一條網絡連接配接,或者屬于另一個程序的信道。
unix有一個統一的i/o模型,在這個模型中,每個信道都以一個整數來命名,它叫做檔案描述符。配置設定給一個信道整數值到底是哪個,通常而言并沒有意義,但要保證stdin、stdout和stderr對應檔案描述符0、1和2,是以保險的做法是,用數字來引用這三個信道。在互動式的終端視窗裡,stdin一般讀取鍵盤的輸入,而stdout和stderr把它們的輸出寫到螢幕上。
大多數指令都接受從stdin來的輸入,并且把自己的輸出寫到stdout,而把出錯消息寫到stderr。有了這樣的約定,使用者就能把指令像積木一樣串起來,建立出混合管道。
shell将<、>和>>解釋成指令,用來把一條指令的輸入或者輸出重新定向到一個檔案。<這個符号把這條指令的stdin和已有的某個檔案的内容聯系起來。符号>和>>則重定向stdout;>會替換檔案的現有内容,而>>則給檔案追加内容。例如,下面的指令
<code>$ echo "this is a test message." &gt; /tmp/mymessage</code>
在/tmp/mymessage這個檔案裡存入一行内容,如果必要,還會建立這個檔案。下面的指令把該檔案的内容用電子郵件發給使用者johndoe。
<code>$ mail -s "mail test" johndoe &lt; /tmp/mymessage</code>
為了把stdout和stderr都重定向到同一個地方,可以用>&這個符号。僅僅重定向stderr的話,則用2>。
指令find示範了想要分開處理stdout和stderr的原因,因為它會在兩個信道提供輸出,特别是以非特權使用者身份運作的時候。例如,像下面這條指令
<code>$ find / -name core</code>
通常會導緻很多“permission denied”這樣的出錯消息,進而把真正的結果給淹沒在混亂的輸出裡了。要消除所有出錯消息,可以用這條指令:
<code>$ find / -name core 2&gt; /dev/null</code>
在這個版本的指令裡,隻有真正比對的結果(該使用者對父目錄有讀權的地方)才會出現在終端視窗中。要把比對路徑的清單儲存在一個檔案裡,可以試試下面的指令:
<code>$ find / -name core &gt; /tmp/corefiles 2&gt; /dev/null</code>
這一行指令把比對的路徑發到/tmp/corefile這個檔案,丢棄出錯消息,向終端視窗什麼都不發。
要把一條指令的stdout連接配接到另一條指令的stdin上,可以用|這個符号,它常叫做管道。下面是一些管道的例子:
第一個例子運作ps産生一份程序清單,由管道送給grep指令選出包含httpd這個詞的若幹行。grep指令的輸出沒有重定向,是以比對的結果都出現在終端視窗裡。
第二個例子用cut指令從/etc/passwd檔案裡把每個使用者的shell的路徑選出來。接着,列出的shell的路徑都通過sort–u進行處理,産生的清單中,路徑名不但依次排序,且路徑名隻出現一次。
要讓第二條指令隻有在第一條指令成功完成之後才執行,可以用一個&&符号把兩條指令隔開。例如:
<code>$ lpr /tmp/t2 &amp;&amp; rm /tmp/t2</code>
這條指令當且僅當/tmp/t2成功送入列印隊列之後,才會删除/tmp/t2。在這裡,lpr指令産生的退出碼為0的話,就算它執行成功,是以,如果讀者已經習慣了其他程式設計語言中的“短路”計算,而這裡用一個表示“邏輯與”的符号,那麼就可能造成混亂。不要想得太多;僅僅把它當做一個shell的習慣用法就行了。
相反,||這個符号表明,隻有前一條指令執行不成功(産生了一個非零的退出碼)時,才執行後面的指令。
在一個腳本裡,可以用反斜線把一條指令分成多行來寫,進而把出錯處理代碼和指令管道的其他部分區分開來:
要實作相反的效果——将多條指令整合在一行裡——可以用分号作為語句分隔符。
2.1.3 變量和引用
變量名在指派的時候沒有标記,但在通路它們的值的時候要在變量名之前加一個$符。例如:
不要在等号兩邊留白白,否則shell會把變量名誤認為是指令名。
引用一個變量的時候,可以用花括号把這個變量的名字括起來,讓分析程式和閱讀代碼的人能清楚地知道變量名的起止位置;例如,用${etcdir}代替$etcdir。正常情況下不要求有這對花括号,但是,如果想要在雙引号引起來的字元串裡擴充變量,它們就會派上用場了。因為人們經常想要在一個變量的内容之後跟着字母或者标點符号。例如:
給shell變量起名字沒有标準的命名規範,但如果變量名的所有字母都大寫,一般表明該變量是環境變量,或者是從全局配置檔案裡讀取的變量。本地變量則多半是所有字母都小寫,而且在變量名的各個部分之間用下劃線隔開。變量名區分大小寫。
環境變量會被自動導入bash的變量名空間,是以它們可以用标準的文法來設定和讀取。指令exportvarname将一個shell變量提升為一個環境變量。用來在使用者登入時設定環境變量的那些指令,都應該放在該使用者的~/.profile或者~/.bash_profile這兩個檔案裡。而其他像pwd(代表目前工作目錄)這樣的環境變量都由shell自動維護。
對于用單引号和雙引号括起來的字元串而言,shell以相似的方式處理它們,例外之處在于雙引号括起來的變量可以進行替換(用*和?這樣的檔案名比對元字元做擴充)和變量擴充。例如:
左引号也叫做撇号,對它的處理和雙引号類似,但是它們還有其他作用,即能夠把字元串的内容按一條shell指令來執行,并且用該指令的輸出來替換這個字元串。例如:
2.1.4 常見的過濾指令
任何“從stdin讀入資料,向stdout輸出結果”這樣循規蹈矩的指令,都可以當作一個過濾器(也就是說,管道的一個環節)來處理資料。在這一小節,我們簡要回顧一些使用較為廣泛的過濾器指令(包括上面已經用到過的一些指令),但是這樣的過濾器指令實際上是無窮無盡的。過濾器指令多面向“集團作戰”,是以有時候它們各自的用處很難單獨展現出來。
大多數過濾器指令都接受在指令行上提供的一個或者多個檔案名作為輸入。隻有在一個檔案都未指定的時候,它們才從自己的标準輸入讀取資料。
cut:把行分成域
cut指令從它的輸入行中選出若幹部分,再列印出來。該指令最常見的用法是提取被限定的若幹域,如2.1.2節裡的例子所示,但是它也能傳回由列邊界所限定的若幹區段。預設的限定符是,但是可以用-d選項改變這個限定符。-f選項指定輸出裡包括哪些域。
參考下面介紹uniq指令一節的内容,了解cut用法的例子。
sort:将行排序
sort指令對輸入行進行排序。簡單吧,不是嗎?或許并不簡單——到底按每行的哪些部分(即“關鍵字”)進行排序,以及進行排序的順序,都可以做精細的調整。表2.1給出了一些比較常見的選項,但要檢視手冊頁才能了解到其他選項。
下面的指令展示出了數值排序和字典排序的不同之處,預設按字典排序。這兩條指令都用了-t:和-k3,3兩個選項,對/etc/group檔案的内容按照由冒号分隔的第三個域(即組id)進行排序。第一條指令按照數值排序,而第二條指令則按照字母排序。
uniq:重複行隻列印一次
uniq指令在思想上和sort -u類似,但它有一些sort不能模拟的選項:-c累計每行出現的次數,-d隻顯示重複行,而-u隻顯示不重複的行。uniq指令的輸入必須先排好序,是以通常把它放在sort指令之後運作。
例如,下面的指令顯示出:有20個使用者把/bin/bash作為自己的登入shell,12個使用者把/bin/false作為登入的shell(後者要麼是僞使用者,要麼就是賬号被禁用的使用者)。
wc:統計行數、字數和字元數
統計一個檔案裡的行數、字數和字元數是另一項常用的操作,wc(表示word count,即字數統計)指令是完成這項操作的一條友善途徑。如果不帶任何參數運作wc,它會顯示全部3種統計結果:
在腳本程式設計的應用場合裡,常給wc指令加上-l、-w或者-c選項,讓它隻輸出一個數。在撇号裡最常出現這種形式的指令,這樣一來,指令的執行結果就可以被儲存起來,或者根據執行結果确定下一步的操作。
tee:把輸入複制到兩個地方
指令的管道一般都是線性的,但是從中間插入管道裡的資料流,然後把一份副本發送到一個檔案裡,或者送到終端視窗上,也往往會很有幫助。用tee指令就能做到這一點,該指令把自己的标準輸入既發送到标準輸出,又發送到在指令行上指定的一個檔案裡。可以把它想成是水管上接的一個三通。
裝置/dev/tty是目前終端的同義語。例如:
<code>$ find / -name core | tee /dev/tty | wc -l</code>
該指令把名叫core的檔案的路徑名,以及找到的core檔案的數量都列印出來了。
把tee指令作為一條執行時間很長的指令管道的最後一條指令,這是一種常見的習慣用法,這樣一來,管道的輸出既可以送到一個檔案,又可以送到終端視窗供使用者檢視。使用者可以預先看到一開始的輸出結果,進而確定一切按預期執行,然後使用者就可以在指令運作的同時不去管它,因為他們知道結果會被儲存下來。
head和tail:讀取檔案的開頭或者結尾
管理者會經常碰到一項操作,即檢視一個檔案開頭或者結尾的幾行内容。這兩條指令預設顯示前10行内容,但使用者可以帶一個指令行參數,指定到底要看多少行内容。
對于互動式的應用場合,head指令已經或多或少被less指令所取代,後者能夠給被顯示的檔案标出頁數,但是head指令仍然在腳本裡大量使用。
tail也有一個不錯的-f選項,對于系統管理者來說,這個選項特别有用。tail -f指令在按要求的行數列印完之後,不是立即退出,而是等着有新行被追加到檔案末尾,再随着新行的出現列印新行——對于監視日志檔案來說很有用。不過要注意,寫檔案的那個程式可能會緩沖它的輸出。即使從邏輯上講,新行是按有規律的時間間隔追加的,但它們可能隻按1kib或者4kib的塊來顯示2。
鍵入即可停止監視。
grep:搜尋文本
grep指令搜尋給它輸入的文本,列印出比對某種模式的行。它的名字源于g/regular-expression/p這條指令,該指令是ed來的(unix系統仍然還有這種編輯器),ed是最早版本的unix上所帶的一種老編輯器。
“正規表達式”是比對文本的模式,它用一種标準的、能準确刻畫的模式比對語言來編寫。盡管在不同的實作上,正規表達式存在輕微的變化,但它們是大多數做模式比對的程式都要用到的一種通用标準。它之是以叫這麼個奇怪的名字,是因為它起源于理論計算研究。我們将在2.3節更詳細地讨論正規表達式。
和大多數過濾器一樣,grep指令有許多選項,這其中包括:列印比對行數的-c、比對時忽略大小寫的-i,以及列印不比對行(而不是比對行)的-v。另一個有用的選項是-l(l的小寫),它讓grep隻列印比對檔案的名字,而不是比對的每一行。例如,下面的指令
表明mdadm的日志項出現在兩個不同的日志檔案裡。
從傳統上看,grep指令是一個相當基礎的正規表達式引擎,但是有些版本的grep能夠選擇其他正規表達式的變體文法。例如,linux上的grep -p指令選擇采用perl風格的表達式,雖然手冊頁警告說,它們還處在“實驗初級階段”。如果需要用這類正規表達式的完整功能,那麼隻能用perl或者python語言。