在 shell 腳本中成功應用 sed 編輯器和 gawk 程序的關鍵在于熟練掌握正則表達式。正則表達式并非易事,從海量數(shù)據(jù)中提取出特定信息往往是一項復雜的任務,因此可能需要耗費一些功夫。本章將詳細探討如何在 sed 編輯器和 gawk 程序中運用正則表達式,從而實現(xiàn)對數(shù)據(jù)的精確過濾。
正則表達式的定義
要理解正則表達式,首先需要明確它們的本質。本節(jié)將介紹正則表達式的概念,并探討在 Linux 中如何運用這一強大工具。
定義
正則表達式是你所定義的模式模板(pattern template),Linux 工具可以用它來過濾文本。Linux 工具(比如 sed 編輯器或 gawk 程序)能夠在處理數(shù)據(jù)時使用正則表達式對數(shù)據(jù)進行模式匹配。如果數(shù)據(jù)匹配模式,它就會被接受并進一步處理;如果數(shù)據(jù)不匹配模式,它就會被濾掉。
正則表達式模式利用通配符來描述數(shù)據(jù)流中的一個或多個字符。Linux 中有很多場景都可以使用通配符來描述不確定的數(shù)據(jù)。在本書之前你已經(jīng)看到過在 Linux 的 ls 命令中使用通配符列出文件和目錄的例子。
星號通配符允許你只列出滿足特定條件的文件,例如:
$?ls?-al?da* -rw-r--r--????1?rich?????rich???????????45?Nov?26?12:42?data -rw-r--r--????1?rich?????rich???????????25?Dec??4?12:40?data.tst -rw-r--r--????1?rich?????rich??????????180?Nov?26?12:42?data1 -rw-r--r--????1?rich?????rich???????????45?Nov?26?12:44?data2 -rw-r--r--????1?rich?????rich???????????73?Nov?27?12:31?data3 -rw-r--r--????1?rich?????rich???????????79?Nov?28?14:01?data4 -rw-r--r--????1?rich?????rich??????????187?Dec??4?09:45?datatest $
da*參數(shù)會讓 ls 命令只列出名字以 da 開頭的文件。文件名中 da 之后可以有任意多個字符(包括什么也沒有)。ls 命令會讀取目錄中所有文件的信息,但只顯示跟通配符匹配的文件的信息。
正則表達式通配符模式的工作原理與之類似。正則表達式模式含有文本或特殊字符,為 sed 編輯器和 gawk 程序定義了一個匹配數(shù)據(jù)時采用的模板。可以在正則表達式中使用不同的特殊字符來定義特定的數(shù)據(jù)過濾模式。
正則表達式的類型
使用正則表達式最大的問題在于有不止一種類型的正則表達式。Linux 中的不同應用程序可能會用不同類型的正則表達式。這其中包括編程語言(Java、perl 和 Python)、Linux 實用工具(比如 sed 編輯器、gawk 程序和 grep 工具)以及主流應用(比如 mysql 和 postgresql 數(shù)據(jù)庫服務器)。
正則表達式是通過正則表達式引擎(regular expression engine)實現(xiàn)的。正則表達式引擎是一套底層軟件,負責解釋正則表達式模式并使用這些模式進行文本匹配。在 Linux 中,有兩種流行的正則表達式引擎:
- POSIX 基礎正則表達式(basic regular expression,BRE)引擎
- POSIX 擴展正則表達式(extended regular expression,ERE)引擎
大多數(shù) Linux 工具都至少符合 POSIX BRE 引擎規(guī)范,能夠識別該規(guī)范定義的所有模式符號。遺憾的是,有些工具(比如 sed 編輯器)只符合了 BRE 引擎規(guī)范的子集。這是出于速度方面的考慮導致的,因為 sed 編輯器希望能盡可能快地處理數(shù)據(jù)流中的文本。
POSIX BRE 引擎通常出現(xiàn)在依賴正則表達式進行文本過濾的編程語言中。它為常見模式提供了高級模式符號和特殊符號,比如匹配數(shù)字、單詞以及按字母排序的字符。gawk 程序用 ERE 引擎來處理它的正則表達式模式。
由于實現(xiàn)正則表達式的方法太多,很難用一個簡潔的描述來涵蓋所有可能的正則表達式。后續(xù)幾節(jié)將會討論最常見的正則表達式,并演示如何在 sed 編輯器和 gawk 程序中使用它們。
定義 BRE 模式
最基本的 BRE 模式是匹配數(shù)據(jù)流中的文本字符。本節(jié)將會演示如何在正則表達式中定義文本以及會得到什么樣的結果。
純文本
前面演示了如何在 sed 編輯器和 gawk 程序中用標準文本字符串來過濾數(shù)據(jù)。通過下面的例子來復習一下。
$?echo?"this?is?a?test"?|?sed?-n?'/test/p' This?is?a?test $?echo?"This?is?a?test"?|?sed?-n?'/trial/p' $ $?echo?"This?is?a?test"?|?gawk?'/test/{print?$0}' This?is?a?test $?echo?"This?is?a?test"?|?gawk?'/trial/{print?$0}' $
第一個模式定義了一個單詞 test。sed 編輯器和 gawk 程序腳本用它們各自的 print 命令打印出匹配該正則表達式模式的所有行。由于 echo 語句在文本字符串中包含了單詞 test,數(shù)據(jù)流文本能夠匹配所定義的正則表達式模式,因此 sed 編輯器顯示了該行。
第二個模式也定義了一個單詞,這次是 trial。因為 echo 語句文本字符串沒包含該單詞,所以正則表達式模式?jīng)]有匹配,因此 sed 編輯器和 gawk 程序都沒打印該行。
你可能注意到了,正則表達式并不關心模式在數(shù)據(jù)流中的位置。它也不關心模式出現(xiàn)了多少次。一旦正則表達式匹配了文本字符串中任意位置上的模式,它就會將該字符串傳回 Linux 工具。
關鍵在于將正則表達式模式匹配到數(shù)據(jù)流文本上。重要的是記住正則表達式對匹配的模式非常挑剔。第一條原則就是:正則表達式模式都區(qū)分大小寫。這意味著它們只會匹配大小寫也相符的模式。
$?echo?"This?is?a?test"?|?sed?-n?'/this/p' $ $?echo?"This?is?a?test"?|?sed?-n?'/This/p' This?is?a?test $
第一次嘗試沒能匹配成功,因為 this 在字符串中并不都是小寫,而第二次嘗試在模式中使用大寫字母,所以能正常工作。
在正則表達式中,你不用寫出整個單詞。只要定義的文本出現(xiàn)在數(shù)據(jù)流中,正則表達式就能夠匹配。
$?echo?"The?books?are?expensive"?|?sed?-n?'/book/p' The?books?are?expensive $
盡管數(shù)據(jù)流中的文本是 books,但數(shù)據(jù)中含有正則表達式 book,因此正則表達式模式跟數(shù)據(jù)匹配。當然,反之正則表達式就不成立了。
$?echo?"The?book?is?expensive"?|?sed?-n?'/books/p' $
完整的正則表達式文本并未在數(shù)據(jù)流中出現(xiàn),因此匹配失敗,sed 編輯器不會顯示任何文本。
你也不用局限于在正則表達式中只用單個文本單詞,可以在正則表達式中使用空格和數(shù)字。
$?echo?"This?is?line?number?1"?|?sed?-n?'/ber?1/p' This?is?line?number?1 $
在正則表達式中,空格和其他的字符并沒有什么區(qū)別。
$?echo?"This?is?line?number1"?|?sed?-n?'/ber?1/p' $
如果你在正則表達式中定義了空格,那么它必須出現(xiàn)在數(shù)據(jù)流中。甚至可以創(chuàng)建匹配多個連續(xù)空格的正則表達式模式。
$?cat?data1 This?is?a?normal?line?of?text. This?is??a?line?with?too?many?spaces. $?sed?-n?'/??/p'?data1 This?is??a?line?with?too?many?spaces. $
單詞間有兩個空格的行匹配正則表達式模式。這是用來查看文本文件中空格問題的好辦法。
特殊字符
在正則表達式模式中使用文本字符時,有些事情值得注意。在正則表達式中定義文本字符時有一些特例。有些字符在正則表達式中有特別的含義。如果要在文本模式中使用這些字符,結果會超出你的意料。
正則表達式識別的特殊字符包括:
.*[]^${}+?|()
隨著本章內容的繼續(xù),你會了解到這些特殊字符在正則表達式中有何用處。不過現(xiàn)在只要記住不能在文本模式中單獨使用這些字符就行了。果要用某個特殊字符作為文本字符,就必須轉義。在轉義特殊字符時,你需要在它前面加一個特殊字符來告訴正則表達式引擎應該將接下來的字符當作普通的文本字符。這個特殊字符就是反斜線()。舉個例子,如果要查找文本中的美元符,只要在它前面加個反斜線。
$?cat?data2 The?cost?is?$4.00 $?sed?-n?'/$/p'?data2 The?cost?is?$4.00 $
由于反斜線是特殊字符,如果要在正則表達式模式中使用它,你必須對其轉義,這樣就產生了兩個反斜線。
$?echo?"?is?a?special?character"?|?sed?-n?'//p' ?is?a?special?character $
最終,盡管正斜線不是正則表達式的特殊字符,但如果它出現(xiàn)在 sed 編輯器或 gawk 程序的正則表達式中,你就會得到一個錯誤。
$?echo?"3?/?2"?|?sed?-n?'///p' sed:?-e?expression?#1,?char?2:?No?previous?regular?expression $
要使用正斜線,也需要進行轉義。
$?echo?"3?/?2"?|?sed?-n?'///p' 3?/?2 $
現(xiàn)在 sed 編輯器能正確解釋正則表達式模式了,一切都很順利。
錨字符
默認情況下,當指定一個正則表達式模式時,只要模式出現(xiàn)在數(shù)據(jù)流中的任何地方,它就能匹配。有兩個特殊字符可以用來將模式鎖定在數(shù)據(jù)流中的行首或行尾。
脫字符(^)定義從數(shù)據(jù)流中文本行的行首開始的模式。如果模式出現(xiàn)在行首之外的位置,正則表達式模式則無法匹配。要用脫字符,就必須將它放在正則表達式中指定的模式前面。
$?echo?"The?book?store"?|?sed?-n?'/^book/p' $ $?echo?"Books?are?great"?|?sed?-n?'/^Book/p' Books?are?great $
脫字符會在每個由換行符決定的新數(shù)據(jù)行的行首檢查模式。
$?cat?data3 This?is?a?test?line. this?is?another?test?line. A?line?that?tests?this?feature. Yet?more?testing?of?this $?sed?-n?'/^this/p'?data3 this?is?another?test?line. $
只要模式出現(xiàn)在新行的行首,脫字符就能夠發(fā)現(xiàn)它。
如果你將脫字符放到模式開頭之外的其他位置,那么它就跟普通字符一樣,不再是特殊字符了:
$?echo?"This?^?is?a?test"?|?sed?-n?'/s?^/p' This?^?is?a?test $
由于脫字符出現(xiàn)在正則表達式模式的尾部,sed 編輯器會將它當作普通字符來匹配。
?
如果指定正則表達式模式時只用了脫字符,就不需要用反斜線來轉義。但如果你在模式中先指定了脫字符,隨后還有其他一些文本,那么你必須在脫字符前用轉義字符。
跟在行首查找模式相反的就是在行尾查找。特殊字符美元符($)定義了行尾錨點。將這個特殊字符放在文本模式之后來指明數(shù)據(jù)行必須以該文本模式結尾。
$?echo?"This?is?a?good?book"?|?sed?-n?'/book$/p' This?is?a?good?book $?echo?"This?book?is?good"?|?sed?-n?'/book$/p' $
使用結尾文本模式的問題在于你必須要留意到底要查找什么。
$?echo?"There?are?a?lot?of?good?books"?|?sed?-n?'/book$/p' $
將行尾的單詞 book 改成復數(shù)形式,就意味著它不再匹配正則表達式模式了,盡管 book 仍然在數(shù)據(jù)流中。要想匹配,文本模式必須是行的最后一部分。
在一些常見情況下,可以在同一行中將行首錨點和行尾錨點組合在一起使用。在第一種情況中,假定你要查找只含有特定文本模式的數(shù)據(jù)行。
$?cat?data4 this?is?a?test?of?using?both?anchors I?said?this?is?a?test this?is?a?test I'm?sure?this?is?a?test. $?sed?-n?'/^this?is?a?test$/p'?data4 this?is?a?test $
sed 編輯器忽略了那些不單單包含指定的文本的行。
第二種情況乍一看可能有些怪異,但極其有用。將兩個錨點直接組合在一起,之間不加任何文本,這樣過濾出數(shù)據(jù)流中的空白行。考慮下面這個例子。
$?cat?data5 This?is?one?test?line. This?is?another?test?line. $?sed?'/^$/d'?data5 This?is?one?test?line. This?is?another?test?line. $
定義的正則表達式模式會查找行首和行尾之間什么都沒有的那些行。由于空白行在兩個換行符之間沒有文本,剛好匹配了正則表達式模式。sed 編輯器用刪除命令 d 來刪除匹配該正則表達式模式的行,因此刪除了文本中的所有空白行。這是從文檔中刪除空白行的有效方法。
點號字符
特殊字符點號用來匹配除換行符之外的任意單個字符。它必須匹配一個字符,如果在點號字符的位置沒有字符,那么模式就不成立。來看一些在正則表達式模式中使用點號字符的例子。
$?cat?data6 This?is?a?test?of?a?line. The?cat?is?sleeping. That?is?a?very?nice?hat. This?test?is?at?line?four. at?ten?o'clock?we'll?go?home. $?sed?-n?'/.at/p' data6 The?cat?is?sleeping. That?is?a?very?nice?hat. This?test?is?at?line?four. $
你應該能夠明白為什么第一行無法匹配,而第二行和第三行就可以。第四行有點復雜。注意,我們匹配了 at,但在 at 前面并沒有任何字符來匹配點號字符。其實是有的!在正則表達式中,空格也是字符,因此 at 前面的空格剛好匹配了該模式。第五行證明了這點,將 at 放在行首就不會匹配該模式了。
字符組
點號特殊字符在匹配某個字符位置上的任意字符時很有用。但如果你想要限定待匹配的具體字符呢?在正則表達式中,這稱為字符組(character class)。可以定義用來匹配文本模式中某個位置的一組字符。如果字符組中的某個字符出現(xiàn)在了數(shù)據(jù)流中,那它就匹配了該模式。
使用方括號來定義一個字符組。方括號中包含所有你希望出現(xiàn)在該字符組中的字符。然后你可以在模式中使用整個組,就跟使用其他通配符一樣。這需要一點時間來適應,但一旦你適應了,效果可是令人驚嘆的。下面是個創(chuàng)建字符組的例子。
$?sed?-n?'/[ch]at/p'?data6 The?cat?is?sleeping. 這里用到的數(shù)據(jù)文件和點號特殊字符例子中的一樣,但得到的結果卻不一樣。這次我們成功濾掉了只包含單詞at的行。匹配這個模式的單詞只有cat和hat。還要注意以at開頭的行也沒有匹配。字符組中必須有個字符來匹配相應的位置。 That?is?a?very?nice?hat. $
這里用到的數(shù)據(jù)文件和點號特殊字符例子中的一樣,但得到的結果卻不一樣。這次我們成功濾掉了只包含單詞 at 的行。匹配這個模式的單詞只有 cat 和 hat。還要注意以 at 開頭的行也沒有匹配。字符組中必須有個字符來匹配相應的位置。
在不太確定某個字符的大小寫時,字符組會非常有用。
$?echo?"Yes"?|?sed?-n?'/[Yy]es/p' Yes $?echo?"yes"?|?sed?-n?'/[Yy]es/p' yes $
可以在單個表達式中用多個字符組。
$?echo?"Yes"?|?sed?-n?'/[Yy][Ee][Ss]/p' Yes $?echo?"yEs"?|?sed?-n?'/[Yy][Ee][Ss]/p' yEs $?echo?"yeS"?|?sed?-n?'/[Yy][Ee][Ss]/p' yeS $
正則表達式使用了 3 個字符組來涵蓋了 3 個字符位置含有大小寫的情況。
字符組不必只含有字母,也可以在其中使用數(shù)字。
$?cat?data7 This?line?doesn't?contain?a?number. This?line?has?1?number?on?it. This?line?a?number?2?on?it. This?line?has?a?number?4?on?it. $?sed?-n?'/[0123]/p'?data7 This?line?has?1?number?on?it. This?line?a?number?2?on?it. $
這個正則表達式模式匹配了任意含有數(shù)字 0、1、2 或 3 的行。含有其他數(shù)字以及不含有數(shù)字的行都會被忽略掉。
可以將字符組組合在一起,以檢查數(shù)字是否具備正確的格式,比如電話號碼和郵編。但當你嘗試匹配某種特定格式時,必須小心。這里有個匹配郵編出錯的例子。
$?cat?data8 60633 46201 223001 4353 22203 $?sed?-n?' >/[0123456789][0123456789][0123456789][0123456789][0123456789]/p >'?data8 60633 46201 223001 22203 $
這個結果出乎意料。它成功過濾掉了不可能是郵編的那些過短的數(shù)字,因為最后一個字符組沒有字符可匹配。但它也通過了那個六位數(shù),盡管我們只定義了 5 個字符組。
記住,正則表達式模式可見于數(shù)據(jù)流中文本的任何位置。經(jīng)常有匹配模式的字符之外的其他字符。如果要確保只匹配五位數(shù),就必須將匹配的字符和其他字符分開,要么用空格,要么像這個例子中這樣,指明它們就在行首和行尾。
$?sed?-n?' >?/^[0123456789][0123456789][0123456789][0123456789][0123456789]$/p >?'?data8 60633 46201 22203 $
現(xiàn)在好多了!本章隨后會看到如何進一步進行簡化。
字符組的一個極其常見的用法是解析拼錯的單詞,比如用戶表單輸入的數(shù)據(jù)。你可以創(chuàng)建正則表達式來接受數(shù)據(jù)中常見的拼寫錯誤。
$?cat?data9 I?need?to?have?some?maintenence?done?on?my?car. I'll?pay?that?in?a?seperate?invoice. After?I?pay?for?the?maintenance?my?car?will?be?as?good?as?new. $?sed?-n?' /maint[ea]n[ae]nce/p /sep[ea]r[ea]te/p '?data9 I?need?to?have?some?maintenence?done?on?my?car. I'll?pay?that?in?a?seperate?invoice. After?I?pay?for?the?maintenance?my?car?will?be?as?good?as?new. $
本例中的兩個 sed 打印命令利用正則表達式字符組來幫助找到文本中拼錯的單詞 maintenance 和 separate。同樣的正則表達式模式也能匹配正確拼寫的 maintenance。
排除型字符組
在正則表達式模式中,也可以反轉字符組的作用。可以尋找組中沒有的字符,而不是去尋找組中含有的字符。要這么做的話,只要在字符組的開頭加個脫字符。
$?sed?-n?'/[^ch]at/p'?data6 This?test?is?at?line?four. $
通過排除型字符組,正則表達式模式會匹配 c 或 h 之外的任何字符以及文本模式。由于空格字符屬于這個范圍,它通過了模式匹配。但即使是排除,字符組仍然必須匹配一個字符,所以以 at 開頭的行仍然未能匹配模式。
區(qū)間
你可能注意到了,我之前演示郵編的例子的時候,必須在每個字符組中列出所有可能的數(shù)字,這實在有點麻煩。好在有一種便捷的方法可以讓人免受這番勞苦。可以用單破折線符號在字符組中表示字符區(qū)間。只需要指定區(qū)間的第一個字符、單破折線以及區(qū)間的最后一個字符就行了。根據(jù) Linux 系統(tǒng)采用的字符集,正則表達式會包括此區(qū)間內的任意字符。現(xiàn)在你可以通過指定數(shù)字區(qū)間來簡化郵編的例子。
$?sed?-n?'/^[0-9][0-9][0-9][0-9][0-9]$/p'?data8 60633 46201 45902 $
這樣可是節(jié)省了不少的鍵盤輸入!每個字符組都會匹配 0~9 的任意數(shù)字。如果字母出現(xiàn)在數(shù)據(jù)中的任何位置,這個模式都將不成立。
同樣的方法也適用于字母。
$?sed?-n?'/[c-h]at/p'?data6 The?cat?is?sleeping. That?is?a?very?nice?hat. $
新的模式[c-h]at 匹配了首字母在字母 c 和字母 h 之間的單詞。這種情況下,只含有單詞 at 的行將無法匹配該模式。
還可以在單個字符組指定多個不連續(xù)的區(qū)間。
$?sed?-n?'/[a-ch-m]at/p'?data6 The?cat?is?sleeping. That?is?a?very?nice?hat. $
該字符組允許區(qū)間 a~c、h~m 中的字母出現(xiàn)在 at 文本前,但不允許出現(xiàn) d~g 的字母。
$?echo?"I'm?getting?too?fat."?|?sed?-n?'/[a-ch-m]at/p' $
該模式不匹配 fat 文本,因為它沒在指定的區(qū)間。
特殊的字符組
除了定義自己的字符組外,BRE 還包含了一些特殊的字符組,可用來匹配特定類型的字符。下面介紹了可用的 BRE 特殊的字符組。
- [[:alpha:]] 匹配任意字母字符,不管是大寫還是小寫
- [[:alnum:]] 匹配任意字母數(shù)字字符 0~9、A~Z 或 a~z
- [[:blank:]] 匹配空格或制表符
- [[:digit:]] 匹配 0~9 之間的數(shù)字
- [[:lower:]] 匹配小寫字母字符 a~z
- [[:upper:]] 匹配任意大寫字母字符 A~Z
- [[:print:]] 匹配任意可打印字符
- [[:punct:]] 匹配標點符號
- [[:space:]] 匹配任意空白字符:空格、制表符、NL、FF、VT 和 CR
可以在正則表達式模式中將特殊字符組像普通字符組一樣使用。
$?echo?"abc"?|?sed?-n?'/[[:digit:]]/p' $ $?echo?"abc"?|?sed?-n?'/[[:alpha:]]/p' abc $?echo?"abc123"?|?sed?-n?'/[[:digit:]]/p' abc123 $?echo?"This?is,?a?test"?|?sed?-n?'/[[:punct:]]/p' This?is,?a?test $?echo?"This?is?a?test"?|?sed?-n?'/[[:punct:]]/p' $
使用特殊字符組可以很方便地定義區(qū)間。如可以用[[:digit:]]來代替區(qū)間[0-9]。
星號
在字符后面放置星號表明該字符必須在匹配模式的文本中出現(xiàn) 0 次或多次。
$?echo?"ik"?|?sed?-n?'/ie*k/p' ik $?echo?"iek"?|?sed?-n?'/ie*k/p' iek $?echo?"ieek"?|?sed?-n?'/ie*k/p' ieek $?echo?"ieeek"?|?sed?-n?'/ie*k/p' ieeek $?echo?"ieeeek"?|?sed?-n?'/ie*k/p' ieeeek $
這個模式符號廣泛用于處理有常見拼寫錯誤或在不同語言中有拼寫變化的單詞。舉個例子,如果需要寫個可能用在美式或英式英語中的腳本,可以這么寫:
$?echo?"I'm?getting?a?color?TV"?|?sed?-n?'/colou*r/p' I'm?getting?a?color?TV $?echo?"I'm?getting?a?colour?TV"?|?sed?-n?'/colou*r/p' I'm?getting?a?colour?TV $
模式中的 u*表明字母 u 可能出現(xiàn)或不出現(xiàn)在匹配模式的文本中。類似地,如果你知道一個單詞經(jīng)常被拼錯,你可以用星號來允許這種錯誤。
$?echo?"I?ate?a?potatoe?with?my?lunch."?|?sed?-n?'/potatoe*/p' I?ate?a?potatoe?with?my?lunch. $?echo?"I?ate?a?potato?with?my?lunch."?|?sed?-n?'/potatoe*/p' I?ate?a?potato?with?my?lunch. $
在可能出現(xiàn)的額外字母后面放個星號將允許接受拼錯的單詞。
另一個方便的特性是將點號特殊字符和星號特殊字符組合起來。這個組合能夠匹配任意數(shù)量的任意字符。它通常用在數(shù)據(jù)流中兩個可能相鄰或不相鄰的文本字符串之間。
$?echo?"this?is?a?regular?pattern?expression"?|?sed?-n?' >?/regular.*expression/p' this?is?a?regular?pattern?expression $
可以使用這個模式輕松查找可能出現(xiàn)在數(shù)據(jù)流中文本行內任意位置的多個單詞。
星號還能用在字符組上。它允許指定可能在文本中出現(xiàn)多次的字符組或字符區(qū)間。
$?echo?"bt"?|?sed?-n?'/b[ae]*t/p' bt $?echo?"bat"?|?sed?-n?'/b[ae]*t/p' bat $?echo?"bet"?|?sed?-n?'/b[ae]*t/p' bet $?echo?"btt"?|?sed?-n?'/b[ae]*t/p' btt $ $?echo?"baat"?|?sed?-n?'/b[ae]*t/p' baat $?echo?"baaeeet"?|?sed?-n?'/b[ae]*t/p' baaeeet $?echo?"baeeaeeat"?|?sed?-n?'/b[ae]*t/p' baeeaeeat $?echo?"baakeeet"?|?sed?-n?'/b[ae]*t/p' $
只要 a 和 e 字符以任何組合形式出現(xiàn)在 b 和 t 字符之間(就算完全不出現(xiàn)也行),模式就能夠匹配。如果出現(xiàn)了字符組之外的字符,該模式匹配就會不成立。
擴展正則表達式
POSIX ERE 模式包括了一些可供 Linux 應用和工具使用的額外符號。gawk 程序能夠識別 ERE 模式,但 sed 編輯器不能。
?
記住,sed 編輯器和 gawk 程序的正則表達式引擎之間是有區(qū)別的。gawk 程序可以使用大多數(shù)擴展正則表達式模式符號,并且能提供一些額外過濾功能,而這些功能都是 sed 編輯器所不具備的。但正因為如此,gawk 程序在處理數(shù)據(jù)流時通常才比較慢。
本節(jié)將介紹可用在 gawk 程序腳本中的較常見的 ERE 模式符號。
問號
問號類似于星號,不過有點細微的不同。問號表明前面的字符可以出現(xiàn) 0 次或 1 次,但只限于此。它不會匹配多次出現(xiàn)的字符。
$?echo?"bt"?|?gawk?'/be?t/{print?$0}' bt $?echo?"bet"?|?gawk?'/be?t/{print?$0}' bet $?echo?"beet"?|?gawk?'/be?t/{print?$0}' $ $?echo?"beeet"?|?gawk?'/be?t/{print?$0}' $
如果字符 e 并未在文本中出現(xiàn),或者它只在文本中出現(xiàn)了 1 次,那么模式會匹配。
與星號一樣,你可以將問號和字符組一起使用。
$?echo?"bt"?|?gawk?'/b[ae]?t/{print?$0}' bt $?echo?"bat"?|?gawk?'/b[ae]?t/{print?$0}' bat $?echo?"bot"?|?gawk?'/b[ae]?t/{print?$0}' $ $?echo?"bet"?|?gawk?'/b[ae]?t/{print?$0}' bet $?echo?"baet"?|?gawk?'/b[ae]?t/{print?$0}' $ $?echo?"beat"?|?gawk?'/b[ae]?t/{print?$0}' $ $?echo?"beet"?|?gawk?'/b[ae]?t/{print?$0}' $
如果字符組中的字符出現(xiàn)了 0 次或 1 次,模式匹配就成立。但如果兩個字符都出現(xiàn)了,或者其中一個字符出現(xiàn)了 2 次,模式匹配就不成立。
加號
加號是類似于星號的另一個模式符號,但跟問號也有不同。加號表明前面的字符可以出現(xiàn) 1 次或多次,但必須至少出現(xiàn) 1 次。如果該字符沒有出現(xiàn),那么模式就不會匹配。
$?echo?"beeet"?|?gawk?'/be+t/{print?$0}' beeet $?echo?"beet"?|?gawk?'/be+t/{print?$0}' beet $?echo?"bet"?|?gawk?'/be+t/{print?$0}' bet $?echo?"bt"?|?gawk?'/be+t/{print?$0}' $
如果字符 e 沒有出現(xiàn),模式匹配就不成立。加號同樣適用于字符組,與星號和問號的使用方式相同。
$?echo?"bt"?|?gawk?'/b[ae]+t/{print?$0}' $ $?echo?"bat"?|?gawk?'/b[ae]+t/{print?$0}' bat $?echo?"bet"?|?gawk?'/b[ae]+t/{print?$0}' bet $?echo?"beat"?|?gawk?'/b[ae]+t/{print?$0}' beat $?echo?"beet"?|?gawk?'/b[ae]+t/{print?$0}' beet $?echo?"beeat"?|?gawk?'/b[ae]+t/{print?$0}' beeat $
這次如果字符組中定義的任一字符出現(xiàn)了,文本就會匹配指定的模式。
使用花括號
ERE 中的花括號允許你為可重復的正則表達式指定一個上限。這通常稱為間隔(interval)。可以用兩種格式來指定區(qū)間。
- 正則表達式準確出現(xiàn) m 次。
- m, n:正則表達式至少出現(xiàn) m 次,至多 n 次。
這個特性可以精確調整字符或字符集在模式中具體出現(xiàn)的次數(shù)。
?
如果你的 gawk 版本過老,gawk 程序不會識別正則表達式間隔。必須額外指定 gawk 程序的–re- interval 命令行選項才能識別正則表達式間隔。
這里有個使用簡單的單值間隔的例子。
$?echo?"bt"?|?gawk?--re-interval?'/be{1}t/{print?$0}' $ $?echo?"bet"?|?gawk?--re-interval?'/be{1}t/{print?$0}' bet $?echo?"beet"?|?gawk?--re-interval?'/be{1}t/{print?$0}' $
通過指定間隔為 1,限定了該字符在匹配模式的字符串中出現(xiàn)的次數(shù)。如果該字符出現(xiàn)多次,模式匹配就不成立。
很多時候,同時指定下限和上限也很方便。
$?echo?"bt"?|?gawk?--re-interval?'/be{1,2}t/{print?$0}' $ $?echo?"bet"?|?gawk?--re-interval?'/be{1,2}t/{print?$0}' bet $?echo?"beet"?|?gawk?--re-interval?'/be{1,2}t/{print?$0}' beet $?echo?"beeet"?|?gawk?--re-interval?'/be{1,2}t/{print?$0}' $
在這個例子中,字符 e 可以出現(xiàn) 1 次或 2 次,這樣模式就能匹配;否則,模式無法匹配
間隔模式匹配同樣適用于字符組。
$?echo?"bt"?|?gawk?--re-interval?'/b[ae]{1,2}t/{print?$0}' $ $?echo?"bat"?|?gawk?--re-interval?'/b[ae]{1,2}t/{print?$0}' bat $?echo?"bet"?|?gawk?--re-interval?'/b[ae]{1,2}t/{print?$0}' bet $?echo?"beat"?|?gawk?--re-interval?'/b[ae]{1,2}t/{print?$0}' beat $?echo?"beet"?|?gawk?--re-interval?'/b[ae]{1,2}t/{print?$0}' beet $?echo?"beeat"?|?gawk?--re-interval?'/b[ae]{1,2}t/{print?$0}' $ $?echo?"baeet"?|?gawk?--re-interval?'/b[ae]{1,2}t/{print?$0}' $ $?echo?"baeaet"?|?gawk?--re-interval?'/b[ae]{1,2}t/{print?$0}' $
如果字母 a 或 e 在文本模式中只出現(xiàn)了 1~2 次,則正則表達式模式匹配;否則,模式匹配失敗。
管道符號
管道符號允許你在檢查數(shù)據(jù)流時,用邏輯 OR 方式指定正則表達式引擎要用的兩個或多個模式。如果任何一個模式匹配了數(shù)據(jù)流文本,文本就通過測試。如果沒有模式匹配,則數(shù)據(jù)流文本匹配失敗。
使用管道符號的格式如下:
expr1|expr2|...
這里有個例子。
$?echo?"The?cat?is?asleep"?|?gawk?'/cat|dog/{print?$0}' The?cat?is?asleep $?echo?"The?dog?is?asleep"?|?gawk?'/cat|dog/{print?$0}' The?dog?is?asleep $?echo?"The?sheep?is?asleep"?|?gawk?'/cat|dog/{print?$0}' $
這個例子會在數(shù)據(jù)流中查找正則表達式 cat 或 dog。正則表達式和管道符號之間不能有空格,否則它們也會被認為是正則表達式模式的一部分。
管道符號兩側的正則表達式可以采用任何正則表達式模式(包括字符組)來定義文本。
$?echo?"He?has?a?hat."?|?gawk?'/[ch]at|dog/{print?$0}' He?has?a?hat. $
這個例子會匹配數(shù)據(jù)流文本中的 cat、hat 或 dog。
管道符號
正則表達式模式也可以用圓括號進行分組。當你將正則表達式模式分組時,該組會被視為一個標準字符。可以像對普通字符一樣給該組使用特殊字符。舉個例子:
$?echo?"Sat"?|?gawk?'/Sat(urday)?/{print?$0}' Sat $?echo?"Saturday"?|?gawk?'/Sat(urday)?/{print?$0}' Saturday $
結尾的 urday 分組以及問號,使得模式能夠匹配完整的 Saturday 或縮寫 Sat。
將分組和管道符號一起使用來創(chuàng)建可能的模式匹配組是很常見的做法。
$?echo?"cat"?|?gawk?'/(c|b)a(b|t)/{print?$0}' cat $?echo?"cab"?|?gawk?'/(c|b)a(b|t)/{print?$0}' cab $?echo?"bat"?|?gawk?'/(c|b)a(b|t)/{print?$0}' bat $?echo?"bab"?|?gawk?'/(c|b)a(b|t)/{print?$0}' bab $?echo?"tab"?|?gawk?'/(c|b)a(b|t)/{print?$0}' $ $?echo?"tac"?|?gawk?'/(c|b)a(b|t)/{print?$0}' $
模式(c|b)a(b|t)會匹配第一組中字母的任意組合以及第二組中字母的任意組合
正則表達式實戰(zhàn)
現(xiàn)在你已經(jīng)了解了使用正則表達式模式的規(guī)則和一些簡單的例子,該把理論用于實踐了。隨后幾節(jié)將會演示 shell 腳本中常見的一些正則表達式例子。
目錄文件計數(shù)
讓我們先看一個 shell 腳本,它會對 PATH 環(huán)境變量中定義的目錄里的可執(zhí)行文件進行計數(shù)。要這么做的話,首先你得將 PATH 變量解析成單獨的目錄名。前面介紹過如何顯示 PATH 環(huán)境變量。
$?echo?$PATH?/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/?local/games $
根據(jù) Linux 系統(tǒng)上應用程序所處的位置,PATH 環(huán)境變量會有所不同。關鍵是要意識到 PATH 中的每個路徑由冒號分隔。要獲取可在腳本中使用的目錄列表,就必須用空格來替換冒號。現(xiàn)在你會發(fā)現(xiàn) sed 編輯器用一條簡單表達式就能完成替換工作。
$?echo?$PATH?|?sed?'s/:/?/g' /usr/local/sbin?/usr/local/bin?/usr/sbin?/usr/bin?/sbin?/bin?/usr/games?/usr/local/games $
分離出目錄之后,你就可以使用標準 for 語句中來遍歷每個目錄。
mypath=$(echo?$PATH?|?sed?'s/:/?/g') for?directory?in?$mypath do ????... done
一旦獲得了單個目錄,就可以用 ls 命令來列出每個目錄中的文件,并用另一個 for 語句來遍歷每個文件,為文件計數(shù)器增值。
這個腳本的最終版本如下。
$?cat?countfiles #!/bin/bash #?count?number?of?files?in?your?PATH mypath=$(echo?$PATH?|?sed?'s/:/?/g') count=0 for?directory?in?$mypath do ????check=$(ls?$directory) ????for?item?in?$check ????????do ????????????count=$[?$count?+?1?] ????????done ????echo?"$directory?-?$count" ????count=0 done $?./countfiles /usr/local/sbin?-?0 /usr/local/bin?-?2 /usr/sbin?-?213 /usr/bin?-?1427 /sbin?-?186 /bin?-?152 /usr/games?-?5 /usr/local/games?–?0 $
現(xiàn)在我們開始體會到正則表達式背后的強大之處了!
驗證電話號碼
前面的例子演示了在處理數(shù)據(jù)時,如何將簡單的正則表達式和 sed 配合使用來替換數(shù)據(jù)流中的字符。正則表達式通常用于驗證數(shù)據(jù),確保腳本中數(shù)據(jù)格式的正確性。
一個常見的數(shù)據(jù)驗證應用就是檢查電話號碼。數(shù)據(jù)輸入表單通常會要求填入電話號碼,而用戶輸入格式錯誤的電話號碼是常有的事。在美國,電話號碼有幾種常見的形式:
(123)456-7890 (123)?456-7890 123-456-7890 123.456.7890
這樣用戶在表單中輸入的電話號碼就有 4 種可能。正則表達式必須足夠強大,才能處理每一種情況。
在構建正則表達式時,最好從左手邊開始,然后構建用來匹配可能遇到的字符的模式。在這個例子中,電話號碼中可能有也可能沒有左圓括號。這可以用如下模式來匹配:
^(?
脫字符用來表明數(shù)據(jù)的開始。由于左圓括號是個特殊字符,因此必須將它轉義成普通字符。問號表明左圓括號可能出現(xiàn),也可能不出現(xiàn)。
緊接著就是 3 位區(qū)號。在美國,區(qū)號以數(shù)字 2 開始(沒有以數(shù)字 0 或 1 開始的區(qū)號),最大可到 9。要匹配區(qū)號,可以用如下模式。
[2-9][0-9]{2}
這要求第一個字符是 2~9 的數(shù)字,后跟任意兩位數(shù)字。在區(qū)號后面,收尾的右圓括號可能存在,也可能不存在。
)?
在區(qū)號后,存在如下可能:有一個空格,沒有空格,有一條單破折線或一個點。你可以對它們使用管道符號,并用圓括號進行分組。
(|?|-|.)
第一個管道符號緊跟在左圓括號后,用來匹配沒有空格的情形。你必須將點字符轉義,否則它會被解釋成可匹配任意字符。
緊接著是 3 位電話交換機號碼。這里沒什么需要特別注意的。
[0-9]{3}
在電話交換機號碼之后,你必須匹配一個空格、一條單破折線或一個點。
(?|-|.)
最后,必須在字符串尾部匹配 4 位本地電話分機號。
[0-9]{4}$
完整的模式如下。
^(?[2-9][0-9]{2})?(|?|-|.)[0-9]{3}(?|-|.)[0-9]{4}$
你可以在 gawk 程序中用這個正則表達式模式來過濾掉不符合格式的電話號碼。現(xiàn)在你只需要在 gawk 程序中創(chuàng)建一個使用該正則表達式的簡單腳本,然后用這個腳本來過濾你的電話薄。腳本如下,可以將電話號碼重定向到腳本來處理。
$?cat?isphone #!/bin/bash #?script?to?filter?out?bad?phone?numbers gawk?--re-interval?'/^(?[2-9][0-9]{2})?(|?|-|.)[0-9]{3}(?|-|.)[0-9]{4}$/{print?$0}' $ $?echo?"317-555-1234"?|?./isphone 317-555-1234 $?echo?"000-555-1234"?|?./isphone $?echo?"312?555-1234"?|?./isphone 312?555-1234 $
或者也可以將含有電話號碼的整個文件重定向到腳本來過濾掉無效的號碼。
$?cat?phonelist 000-000-0000 123-456-7890 212-555-1234 (317)555-1234 (202)?555-9876 33523 1234567890 234.123.4567 $?cat?phonelist?|?./isphone 212-555-1234 (317)555-1234 (202)?555-9876 234.123.4567 $
只有匹配該正則表達式模式的有效電話號碼才會出現(xiàn)。
解析郵件地址
如今這個時代,電子郵件地址已經(jīng)成為一種重要的通信方式。驗證郵件地址成為腳本程序員的一個不小的挑戰(zhàn),因為郵件地址的形式實在是千奇百怪。郵件地址的基本格式為:
username@hostname
username 值可用字母數(shù)字字符以及以下特殊字符:
- 點號
- 單破折線
- 加號
- 下劃線
在有效的郵件用戶名中,這些字符可能以任意組合形式出現(xiàn)。郵件地址的 hostname 部分由一個或多個域名和一個服務器名組成。服務器名和域名也必須遵照嚴格的命名規(guī)則,只允許字母數(shù)字字符以及以下特殊字符:
- 點號
- 下劃線
服務器名和域名都用點分隔,先指定服務器名,緊接著指定子域名,最后是后面不帶點號的頂級域名。
頂級域名的數(shù)量在過去十分有限,正則表達式模式編寫者會嘗試將它們都加到驗證模式中。然而遺憾的是,隨著互聯(lián)網(wǎng)的發(fā)展,可用的頂級域名也增多了。這種方法已經(jīng)不再可行。
從左側開始構建這個正則表達式模式。我們知道,用戶名中可以有多個有效字符。這個相當容易。
^([a-zA-Z0-9_-.+]+)@
這個分組指定了用戶名中允許的字符,加號表明必須有至少一個字符。下一個字符很明顯是@,沒什么意外的。
hostname 模式使用同樣的方法來匹配服務器名和子域名。
([a-zA-Z0-9_-.]+)
這個模式可以匹配文本:
server server.subdomain server.subdomain.subdomain
對于頂級域名,有一些特殊的規(guī)則。頂級域名只能是字母字符,必須不少于二個字符(國家或地區(qū)代碼中使用),并且長度上不得超過五個字符。下面就是頂級域名用的正則表達式模式。
.([a-zA-Z]{2,5})$
將整個模式放在一起會生成如下模式。
^([a-zA-Z0-9_-.+]+)@([a-zA-Z0-9_-.]+).([a-zA-Z]{2,5})$
這個模式會從數(shù)據(jù)列表中過濾掉那些格式不正確的郵件地址。現(xiàn)在可以創(chuàng)建腳本來實現(xiàn)這個正則表達式了。
$?echo?"rich@here.now"?|?./isemail rich@here.now $?echo?"rich@here.now."?|?./isemail $ $ echo?"rich@here.n"?|?./isemail $ $?echo?"rich@here-now"?|?./isemail $ $?echo?"rich.blum@here.now"?|?./isemail rich.blum@here.now $?echo?"rich_blum@here.now"?|?./isemail rich_blum@here.now $?echo?"rich/blum@here.now"?|?./isemail $ $?echo?"rich#blum@here.now"?|?./isemail $ $?echo?"rich*blum@here.now"?|?./isemail $