本節書摘來自異步社群《unix/linux 系統管理技術手冊(第四版)》一書中的第2章,第2.4節,作者:【美】evi nemeth , garth snyder , trent r.hein , ben whaley著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視
unix/linux 系統管理技術手冊(第四版)
larry wall發明了perl語言,它第一種真正偉大的腳本程式設計語言。它的能耐要比bash大得多,而且編寫良好的perl代碼也相當容易閱讀。另一方面,perl沒有給開發人員強加太多的風格規範,是以不考慮可讀性的perl代碼顯得很神秘。perl也被诟病為隻适合寫(不适合讀)的語言。
我們在這裡介紹perl 5,這個版本成為标準已經有10年了。perl 6是一個仍處在開發之中的主要版本。參考perl6.org了解詳情。
對于系統管理工作來說,perl或者python(2.5節開始讨論)都是比傳統的程式設計語言(如c、c++、c#和java)更好的選擇。它們做得更多,但代碼行數更少,程式員調試的痛苦更少,而且省卻了編譯的麻煩。
選擇哪種語言通常取決于個人的偏好,或者取決于雇主強加給雇員的标準。perl和python都提供了由使用者群編寫的子產品庫和語言擴充庫。perl存在的年頭更長,是以它提供的庫長尾效應更為明顯[譯者注:即功能繁多影響力不大的庫數量更龐大,累積起來的效應也更大]。不過,對于常見的系統管理任務而言,兩者的支援庫大緻上等價。
perl的口号是“條條大路通羅馬。”要記住本節讀到的大多數例子都能用别的方法來實作。
perl的語句用分号分隔1。注釋以一個井号(#)開頭,并且一直到這一行的結尾。語句塊用花括号括起來。下面是一個簡單的“hello,world!”程式:
和bash程式一樣,必須用chmod +x将這個檔案改為可執行,或者直接調用perl的解釋程式執行它。
perl腳本中的代碼行都不是shell指令;它們是perl代碼。bash可以讓使用者把一系列指令組合起來,把它叫做腳本,但perl和bash不一樣,除非讓perl去看外面的世界,否則perl自己不會看。也就是說,perl提供了許多和bash一樣的慣例,如使用撇号來獲得一條指令的輸出結果。
2.4.1 變量和數組
perl有3種基本資料類型:标量(也就是說,像數和字元串這樣的一進制量)、數組和哈希(hash)。哈希也叫做關聯數組。變量的類型總是一目了然,因為它展現在變量名上:标量的變量以$開頭,數組變量以@開頭,而哈希變量以%開頭。
在perl語言裡,“清單(list)”和“數組(array)”這兩個術語經常混用,不過或許更準确的說法是,清單是一系列的值,而數組是能夠儲存這樣一個清單的變量。數組裡的各個元素都是标量,是以和普通的标量變量一樣,它們的名字都以$開頭。數組下标從零開始,數組@a裡元素的最大下标是$#a。這個值加1就等于數組的長度。
數組@argv儲存着該腳本的指令行參數。可以像通路其他任何數組那樣來通路它。
下面的腳本展示了數組的用法:
輸出為:
僅僅在這幾行代碼中,看點就很多。冒着分散我們注意力的風險,我們在所舉的每個perl例程中都包含了幾種常見的習慣用法。在每個例子之後的文字中,我們都會闡述其中的竅門。如果仔細閱讀這些例子(不要膽怯,它們都不長!),在看完本章的内容之後,就會對perl最常碰到的形式有了一些經驗。
2.4.2 數組和字元串文字
在本例中,首先要注意(…)建立了一個清單。這個清單裡的每個元素都是字元串,它們由逗号隔開。建立好這個清單之後,就把它指派給變量@items。
perl不嚴格要求所有的字元串都要用引号引起來。在這個具體的例子裡,沒有引号也能給@items賦初值。
perl把這些沒有用引号引起來的字元串稱為“裸單詞(bareword)”,它們就按上次通路的方式來解釋。如果用任何其他方式解釋都沒有意義,perl就嘗試把它按一個字元串來解釋。在有限的幾種情況下,這樣解釋有意義,并且能讓代碼保持整潔。不過,這裡不一定正好就是上述幾種情況之一。即使自己能一直保持用引号把字元串引起來,也要有所準備,去分析别人寫的沒有用引号引起來的代碼。
要初始化這個數組,更perl化的辦法是用qw(指quote words的縮寫)操作符。它實際上是把字元串用引号引起來的一種形式,而且和perl裡大多數被引号引起來的實體一樣,可以選擇自己的限定符。下面的形式
是最傳統的辦法,但是它容易誤導人,因為qw之後的部分不再是一個清單了,它實際上是一個用空白分隔字元串形成的一個清單。下面的這個版本的寫法
也能用,而且對于真正要做什麼來說,這種寫法可能還更正确一點兒。注意逗号沒了,因為qw已經包括了它們的功能。
2.4.3 函數調用
print和printf都能接受任意數量的參數,這些參數由逗号分隔。但是join(…)看上去更像是某種函數調用;它和print和printf有怎樣的不同呢?
實際情況并非如此;print、printf和join都是普通的函數。如果不會引起歧義,perl允許省略函數調用的圓括号,是以兩種調用形式(帶括号和不帶括号)都很常用。在上面例子的print那句中,圓括号把join的參數同print的參數給區分開了。
我們可以說表達式@items[0,1]必定要按某種清單來算,因為它以@開頭。這實際上是一個“數組段”或者叫子數組,0和1兩個下标列出了在這個數組段裡包含的數組元素的索引。perl在這裡也能接受一個範圍值,就像其等價表達式@items[0..1]裡出現的那樣。這裡也能接受單個數值下标:@items[0]是一個清單,裡面隻有一個标量,即字元串“socks”。在這種情況下,它等價于("socks")這個常量。
函數調用會自動擴充數組,如下面的表達式中
join接收到3個字元串參數:“and”、“socks”和“shoes”。它把第二個及後面的參數連起來,在兩個參數之間插入第一個參數。結果是“socks and shoes”。
2.4.4 表達式裡的類型轉換
在printf那一行,$#items + 1計算得到數字3。由此看來,$#items是一個數值,但這并不是這個表達式要按數值計算的原因;"2" + 1也是可以的。關鍵在于運算符+,它總是暗指算術運算。它把自己的參數轉為數字,并計算得到數值結果。類似地,點運算符(.)把兩個字元串連接配接起來,它根據需要轉換自己的操作數:"2" . (12 ** 2)得到“2144”。
2.4.5 字元串表達式和變量
和bash裡的情況一樣,雙引号引起來的字元串可以進行變量擴充。和bash裡一樣的還有,如果必要,可以用花括号把變量名括起來,如${items[2]},進而避免歧義(這裡的花括号隻用于示範;它們不是必須的)。$暗示這個表達式應該按标量來算。@items是數組,但是它的任何一個元素都是标量,起名字的習慣就反映出了這一事實。
2.4.6 哈希
哈希(也稱為關聯數組)表示一組“鍵/值”對。可以把一個哈希當作下标(鍵)是任意标量值的數組;它們不一定是數字。但在實際中,數字和字元串都是常用的鍵。
哈希變量的第一個字元是%(例如,%myhash),但是和在數組中的情況一樣,哈希裡的單個值都是标量,是以也以$開頭。哈希的下标用花括号而不是方括号括起來,例如$myhash{'ron'}。
哈希是系統管理者的一個重要工具。系統管理者編寫的幾乎每個腳本都會用到它們。在下面的代碼中,我們會讀取一個檔案的内容,按照/etc/passwd裡的規則分析它,然後用分析得到的若幹項結果構造一個叫做%names_by_uid的哈希。這個哈希中每一項都是使用者名和它對應的uid。
和上面的那個示例腳本一樣,我們在這幾行代碼裡也加入了幾點新思想。在開始介紹這幾處細節之前,先看一下這個腳本的輸出:
while ($ = <>)這條語句一次一行讀取輸入,把它指派給名為$的變量;和c語言一樣,整個指派語句的值是等号右邊的值。當碰到輸入的結尾時,<>傳回一個出錯值,然後結束循環。
perl對<>的解釋為,檢查指令行看是否在那裡給出了任何檔案。如果提供了檔案,那麼它就依次打開每個檔案,然後在循環裡運作這個檔案的内容。如果沒有在指令行給出任何檔案,那麼perl就考慮從标準輸入獲得輸入。
在循環體内,把split傳回的值指派給一系列變量,split是個函數,它用傳給它的正規表達式作為域分隔符,對輸入字元串進行分隔。這裡的正規表達式用斜線來界定;這隻是另一種形式的引用符号,這種形式的引用專門用于正規表達式,但和雙引号的解釋很類似。我們同樣也可以寫為split ':'或者split ":"。
這個split要用冒号分割的字元串到底是什麼,沒有明确地指定。當split沒有第二個參數的時候,perl就認為要分割$_的值。說實話,即使這個比對模式(即用冒号做分隔符)也是可有可無的;預設用空白做分隔符,但卻會忽略開頭的所有空白。
不過稍等一下,還要多說一點。甚至回到循環一開始的地方,給$_指派的操作也是不必要的。如果簡單地寫
那麼perl會自動把每行都儲存在$裡。使用者不必明确地去通路儲存輸入行的變量,就可以處理這些行。把$用作預設操作數的做法很常見,隻要在$或多或少有意義的地方,perl都會允許用$。
在獲得passwd檔案内各個域值的多重指派語句裡
等式左邊出現的清單建立了split的“清單上下文”,告訴split傳回結果是由所有的域構成的一個清單。如果是給一個标量指派,例如,
split則運作在“标量上下文”裡,隻傳回它找到的域的個數。通過使用wantarray函數,使用者自己編寫的函數也能區分标量上下文和清單上下文。該函數在清單上下文中傳回一個真值,而在标量上下文中傳回一個假值,在空(void)上下文裡則傳回一個不定值。
the line
下面這行代碼
也有一些深意。清單上下文裡的哈希(這裡是reverse函數的一個參數)算成(key1, value1, key2, value2, …)形式的一個清單。reverse函數颠倒該清單的次序,得到(valuen, keyn, …, value1, key1)。最後,給哈希變量%uids_by_name指派的時候把這個清單按(key1, value1, …)這樣轉換,是以得到了颠倒的索引。
2.4.7 引用和自動生成
雖然這兩方面都是進階話題,但是如果我們不提一下它們的話,那就是我們的工作怠慢了。這裡給出它們的簡短總結。數組和哈希隻儲存标量,但使用者經常想在其中再儲存别的數組和哈希。例如,回到我們前面分析/etc/passwd檔案的那個例子,使用者可能想要把passwd檔案裡每一行所有的域都儲存到一個用uid來索引的哈希結構裡。
雖然不能直接儲存數組和哈希,但是卻可以儲存對數組和哈希的引用,因為引用本身是标量。隻要在變量名之前加上一個反斜線(例如,@array),或是用引用數組或引用哈希的文法,就可以建立對數組或者哈希的引用。例如,分析密碼的循環就可以變成下面的樣子:
尖括号傳回對一個數組的引用,這個數組裡儲存有分割後的結果。$array_ref->[2]的記法引用uid域,即$array_ref所指數組中的第三個成員。
這裡不能用$array_ref[2],因為我們沒有定義@array_ref這個數組;$array_ref和@array_ref是不同的變量。再進一步說,如果在這裡錯誤地用了$array_ref[2],那麼也不會得到出錯消息,因為@array_ref是一個完全合法的數組名;隻是不能給它指派而已。
缺少報警消息似乎是個問題,但它可以說是perl最好的特性之一,這個特性叫做“自動生成(autovivification)”。因為變量名和引用文法總是會清楚地表明要通路的資料結構,是以就不用手工随時建立任何資料結構了。隻要進行最低可能層面的指派操作,就會自動生成中間結構。例如,隻用一次指派操作,就可以建立一個指向數組的哈希結構,數組的内容又是指向哈希的引用。
2.4.8 perl語言裡的正規表達式
用=~操作符把字元串“綁定”到正規表達式上,就可以在perl裡使用正規表達式了。例如,下面這一行代碼
檢查儲存在$text裡的字元串,看是否能夠比對正規表達式ab+c。要對預設字元串$_進行操作,隻要省略掉變量名和綁定操作符即可。實際上,還可以省去m,因為預設就是執行比對操作:
替換操作也和比對操作類似:
我們插入了一個g選項,用“and so on”替換所有出現的“etc.”,而不是隻替換第一次出現的“etc.”。其他常見的選項有忽略大小寫的i、用點号(.)比對換行的s,還有m,即用^和$比對各行行首和行尾,而不隻是要搜尋的文本的開頭和結尾。
下面這個腳本示範了另外兩個要點:
該腳本的輸出為:
這個例子展示了由//括起來的變量怎樣做擴充,這樣一來,正規表達式就不必是一個固定的字元串了。qq是雙引号操作符的另一種寫法。
在執行一次比對或者替換操作之後, $1、$2等變量的内容就和正規表達式裡括号中捕獲的内容相對應。這些變量的内容在做替換操作時也能用,此時用1、2等來引用它們。
2.4.9 輸入和輸出
打開一個檔案執行讀或者寫操作時,就要定義一個“檔案句柄”來辨別這個通道。在下面的例子裡,infile是/etc/passwd的檔案句柄,而outfile是關聯到/tmp/passwd的檔案句柄。while語句的循環條件是,它和我們已經見過的<>很相似,但它專指一個檔案句柄。這條語句讀取檔案句柄infile中的每一行,直到檔案結尾,這時while循環結束。每行的内容都放入$_變量。
如果成功打開了這個檔案,則open傳回一個真值,進而繞過對die子句的判斷(讓這個子句不必執行)。perl的or操作符和||(perl也有這個操作符)的作用很像,但是優先級更低。如果要特意先對操作符左邊的全部内容做判斷,然後perl才關注失敗的結果,那麼采用操作符or一般是更好的選擇。
perl用來指定如何使用每個檔案(讀?寫?追加?)的文法和shell一模一樣。還可以用像"/bin/df |"這樣的“檔案名”來打開和shell指令聯系的管道。
2.4.10 控制流程
下面的例子用perl重新實作了一個bash腳本,我們早先用後者來驗證指令行參數是否有效。讀者可以參照2.2.3節裡該腳本的bash版本。注意,perl的if語句結構沒有then這個關鍵字,也沒有終結關鍵字,它隻是一個用花括号括起來的語句塊。
還可以在單個語句的後面追加if子句(或者unless子句,它是if子句的否定版本),使得該語句有條件地執行。
在這個例子中,有兩行代碼使用了perl的單目操作符-d,用來判斷$source_dir和$dest_dir是否為目錄。第二種形式(-d操作符在行首)有優勢,它把實際的判斷語句放在行首,這個位置最顯眼。不過,用or來表示“否則”的意思有點兒難懂;有些讀到這一代碼的人可能會發現它容易讓人誤會。
按标量上下文(本例中由标量運算符來指出)取數組變量的值,傳回的是該數組内元素的個數。這個值比$#array的值正好多1;在perl裡,有不止一種方法可以得到這個值。
perl的函數從名為@_的數組裡獲得它們的參數。用shift操作符是通路這些參數最常用的方法,shift操作符删除參數數組裡的第一個元素,并傳回它的值。
這個版本的show_usage函數接受一則要列印的出錯消息,但也可以不提供這個出錯消息。如果提供了一則出錯消息,那麼還可以提供一個特殊的退出碼。三目操作符?:計算其中第一個參數的值;如果值為真,那麼傳回結果就是第二個參數;否則就傳回第三個參數。
和bash裡一樣,perl也有一種專門的“else if”條件,但是它的關鍵字是elsif而不是elif(對于bash和perl兩種語言都用的人來說,這些有意思的小差别要麼讓人頭腦聰穎,要麼讓人抓狂)。
如表2.5所示,perl的比較運算符正好和bash的相反;字元串比較用文本運算符,而數值比較用傳統的代數表達式。讀者可以和2.2.7節的表2.2進行比較。

在perl裡,可以使用表2.3中除-nt和-ot之外的所有檔案測試操作符,-nt和-ot隻有bash才支援。
和bash一樣,perl也有兩種類型的循環。比較常見的循環形式是通過一個明确的參數清單。例如,下面的代碼通過一個動物的清單來循環,每行列印一個動物的名字。
我們給出了傳統的for和foreach兩種寫法,但實際上,它們兩個在perl裡是相同的關鍵字,可以根據自己的偏好選用其中任何一個。
在perl 5.10(2007年)版之前都沒有明确的case或者switch語句,但可以用幾種辦法取得相同的效果。用多層嵌套的if語句顯然是一種辦法,但除了這種不太好的做法之外,另一種可能的方法是用一條for語句設定$_的值,然後提供一種上下文,讓last語句可以從中跳出來:
用多個正規表達式和$_裡儲存的參數進行比較。比對不成功的話,就繞過&&并直接落入下一個測試條件。隻要比對了一個正規表達式,那麼就執行相應的do語句塊。然後由last語句從for語句塊裡立即跳出。
2.4.11 接受和确認輸入
下面的腳本把我們在前面碰到的許多perl結構都結合了起來,包括例程(函數)、字尾形式的if語句,以及for循環。這個程式本身隻是主函數get_string的一個封裝程式,get_string是一個檢查輸入有效性的通用例程。這個例程提示輸入一個字元串,删除結尾的所有換行符,然後核對該字元串是否為空。空字元串會提示重新輸入,三次之後腳本就放棄嘗試。
這個腳本的輸出為:
在函數get_string和for循環中,都用my操作符建立有局部作用域的變量。在預設情況下,perl中的變量都是全局變量。
對get_string裡的局部變量清單進行初始化的時候,隻從該函數的參數數組中獲得了一個标量。對于初始化清單中的變量,如果沒有獲得相應的值(本例中是$response),那麼就保持未定義的狀态。
傳給函數readline的*stdin是一個“類型通配(typeglob)”,這在語言設計上是一個讓人讨厭的缺點。最好不要太深入地去搞清楚它的确切含義,以免搞得自己頭大。簡單解釋說,就是perl的檔案句柄不是一流的資料類型,是以一般必須在它們的名字之前加一個星号,才能當做參數傳給函數。
在給$fname和$lname指派的語句裡,uc(代表“convert to uppercase”,即轉為大寫)和get_string兩個函數調用都不帶括号。因為在一條語句裡不可能出現混淆,是以這樣寫沒問題。
2.4.12 perl用作過濾器
不通過腳本也能用perl,隻要在指令行寫單獨的表達式就行。這是做快速文本轉換的一種好方法,這種方法幾乎取代了比較老的過濾器程式,如sed、awk和tr。
用指令行選項-pe可以對stdin循環處理,對每行做一次簡單比對,然後列印結果。例如,下面這條指令
把/etc/passwd裡每行末尾的/bin/sh替換為/bin/bash,然後把轉換後的passwd檔案送到stdout。看到用斜線作為定界符(例如,s/foo/bar/),配合文本替換操作符一起使用,這種形式對讀者來說可能更習慣,但是perl允許用任何字元。在這裡,搜尋文本和替換文本都帶有反斜線,是以用#作為定界符更簡單。如果用成對的定界符,那麼必須用4個,而不是通常的3個,例如,s(foo)(bar)。
perl的-a選項打開了自動分隔模式,這種模式把輸入行分成若幹個域,儲存在名為@f的數組裡。預設的域分隔符是空白,但可以用-f選項設定另一種分隔符模式。
自動分隔模式和-p或者-n(-p的變體,它表示不自動列印)配合使用很友善。例如,下面的前兩條指令用perl -ane把df兩種不同形式的輸出進行分隔。第三行指令接着執行join操作,把兩組結果按filesystem域拼接到一起,生成的組合表裡包括兩個df輸出版本中的所有域。
不用臨時檔案的腳本如下:
真正厲害的地方在于,可以把-i和-pe連起來用,就地編輯檔案;perl把檔案讀進來,提供檔案裡要編輯的那些行,然後再把結果儲存回原來的檔案。可以給-i提供一個模式,告訴perl怎樣備份每個檔案原來的版本。例如,-i.bak把passwd檔案備份為passwd.bak。要小心——如果沒有提供一個備份模式,那麼就根本得不到備份了。注意,在-i和字尾名之間沒有空格。
2.4.13 perl的附加子產品
位于cpan.org的cpan(comprehensive perl archive network,綜合perl存檔網絡)是一個perl語言的大寶庫,裡面有使用者貢獻的第三方庫。用cpan指令安裝新的子產品非常簡單,就像把yum或者apt這樣的包管理軟體用到perl子產品上一樣。如果在linux系統上,那麼檢查一下,看該linux發行版本是否把要找的子產品已經放在軟體包裡,成為一個标準的功能——在系統層面隻安裝一次,然後讓系統以後負責該子產品的更新,這種做法更簡單。
在沒有cpan指令的系統上,可以運作perl -mcpan -e shell,從另一條途徑達到同樣的效果:
使用者還可以把perl子產品安裝到自己的主目錄裡,供個人使用,但是這個過程并不簡單。我們推薦采用一種自由的政策,把cpan的第三方子產品安裝到系統層面上;perl社群提供了一個釋出子產品的中心點,放開代碼供人們檢查,貢獻子產品的人都要給出自己的名字。perl子產品不會比任何别的開源代碼更危險。
為了獲得更好的性能,許多perl子產品都用到了c寫的元件。安裝這樣的子產品就會涉及去編譯這些元件,是以需要一個完整的開發環境,其中要包括c編譯器和一整套庫。
和大多數語言一樣,perl程式裡最常見的錯誤是,重複實作已經由社群編寫的子產品所提供的功能2。養成習慣,在解決任何perl的問題時首先通路cpan,這樣做會節省開發和調試的時間。