學習Linux,101:使用正規表示式搜尋文字檔案
本文深入介紹基礎的 Linux 程式管理技術。您將學習如何:
- 建立簡單的正規表示式
- 使用正規表示式搜尋檔案和檔案系統
- 使用正規表示式和 sed
本文幫助您準備 Linux Professional Institute`s Junior Level Administration (LPIC-1) 考試的 103 主題下的 103.7 考核目標。該考核目標的權值為 2。
為了從本文獲得最大的收益,您應該具備基礎的 Linux 知識,並且具有一個能夠正常執行的 Linux 系統,以便練習本文討論的命令。不同版本的程式輸出的結果的格式可能不同,因此您的結果可能與本文圖片和清單所示的結果有所不同。本文以之前的文章 “學習 Linux,101:文字流和過濾器” 中討論的概念為基礎。
在本文中,我們將使用文章 “學習 Linux,101:文字流和過濾器” 中建立的檔案練習命令。如果您沒有學習該文或者沒有儲存得到的檔案,您可以在名為 lpi103-7 的子目錄中新建一個子目錄,並建立必要的檔案。開啟文字視窗,使用主目錄作為當前目錄。然後,將清單 1 中的內容複製到視窗執行命令,這將建立 lpi103-7 子目錄以及您將使用的檔案。
mkdir -p lpi103-7 && cd lpi103-7 && { echo -e "1 apple 2 pear 3 banana" > text1 echo -e "9 plum 3 banana 10 apple" > text2 echo "This is a sentence. " !#:* !#:1->text3 split -l 2 text1 split -b 17 text2 y; cp text1 text1.bkp mkdir -p backup cp text1 backup/text1.bkp.2 } |
您的視窗應類似於清單 2,當前目錄現在是 lpi103-7 目錄中新建的目錄。
ian@attic4:~$ mkdir -p lpi103-7 && cd lpi103-7 && { > echo -e "1 apple 2 pear 3 banana" > text1 > echo -e "9 plum 3 banana 10 apple" > text2 > echo "This is a sentence. " !#:* !#:1->text3 echo "This is a sentence. " "This is a sentence. " "This is a sentence. ">text3 > split -l 2 text1 > split -b 17 text2 y; > cp text1 text1.bkp > mkdir -p backup > cp text1 backup/text1.bkp.2 > } ian@attic4:~/lpi103-7$ |
正規表示式在計算機語言理論中有很長的歷史。大部分計算機學科的學生都知道,可以使用正規表示式表示的語言與有限時序機(finite automata)可以接受的語言一樣。本文中的正規表示式所代表的含義更為複雜,與您在電腦科學課堂上學到的內容可能不同,雖然傳承是一樣的。
正規表示式(也稱為 “regex” 或 “regexp”)是描述文字字串的一種方式或者一種模式,程式可以根據任何文字字串匹配 該模式,以提供強大的搜尋功能。grep
(正規表示式處理程式的縮寫)是 Linux 或 UNIX® 程式設計師或管理員的標準裝備,他們可以在檔案搜尋或命令輸出中使用正規表示式。在文章 “學習
Linux,101:文字流和過濾器” 中,我們介紹了 sed
(流編輯器的縮寫),這是使用正規表示式在檔案或文字流中查詢和替換文字的另一個標準工具。本文將幫助您更好地理解
grep
和 sed
使用的正規表示式。使用正規表示式的另一個程式是 awk
。
結合本系列文章中的其他部分您會發現,整本書都是以正規表示式和計算機語言理論為基礎的。更多建議請參見
參考資料。
根據您對正規表示式的瞭解,您可能發現正規表示式語法與 “學習 Linux,101:檔案和目錄管理” 中討論的萬用字元語法有類似之處。但這種相似之處只是表面現象。
大部分 Linux 系統中的 GNU 程式可以使用兩種常規表示式語法:basic 和 extended。使用 GNU grep,功能上沒有不同之處。本文將介紹基本的語法,以及它和擴充套件語法之間的不同之處。
正規表示式通過元字元 加強的字元 和操作符 構建。大部分字元與自身匹配,大部分元字元必須使用反斜槓()進行轉義。基本的操作包括:
- 連線
- 連線兩個正規表示式建立一個更長的正規表示式。例如,正規表示式 a 匹配字串 abcdcba 兩次,正規表示式
b 也是一樣。但是,ab 將只匹配 abcdcba,而
ba 將只匹配 abcdcba。 - 重複
- Kleene * 或重複操作符將匹配 0 次或多次前一個正規表示式。因此像 a*b 之類的表示式將匹配以任何以
a 開頭以 b 結尾的字串,包括 b 本身。Kleene * 不用轉義,因此希望匹配字面值星號(*)的表示式必須讓星號轉義。這裡使用的 * 與萬用字元中使用的 * 不同,萬用字元中的 * 號匹配任何字串。 - 交替
- 交替操作符(|)匹配前置或後置表示式。它必須匹配前一個或後一個表示式之一。在基本語法中它必須轉義。例如,表示式 a*|b*c 匹配由任何數量的
a 或 b 組成(但不是同時)且以一個 c 結尾的字串。同樣,單個字元
c 也是匹配的。
儘量不要引用正規表示式以避免 shell 膨脹。
我們將之前的示例中建立的使用文字檔案(參見 “設定示例”)。研究清單 3 中的示例。注意
grep
使用一個正規表示式作為引數,還有 0 個或多個要搜尋的檔案。如果沒有給定檔案,grep 將搜尋 stdin,這讓它成為一個可以在管道中使用的過濾器。如果沒有匹配任何行,則
grep
沒有輸出,儘管可以測試它的退出程式碼。
ian@attic4:~/lpi103-7$ grep p text1 1 apple 2 pear ian@attic4:~/lpi103-7$ grep pea text1 2 pear ian@attic4:~/lpi103-7$ grep "p*" text1 1 apple 2 pear 3 banana ian@attic4:~/lpi103-7$ grep "pp*" text1 1 apple 2 pear ian@attic4:~/lpi103-7$ grep "x" text1; echo $? 1 ian@attic4:~/lpi103-7$ grep "x*" text1; echo $? 1 apple 2 pear 3 banana 0 ian@attic4:~/lpi103-7$ cat text1 | grep "l|n" 1 apple 3 banana ian@attic4:~/lpi103-7$ echo -e "find an s* here" | grep "s*" s* here |
從上例中可以看出,有時候會得到出乎意料的結果,尤其是使用重複的時候。您可能預期 p* 或者 pp* 能夠匹配幾個帶
p 的字串,但是 p* 和 x* 能匹配檔案的所有行,因為 * 操作符匹配
0 次或多次前一個正規表示式。
有兩個示例演示了從 grep 退出的程式碼。如果找到匹配,則返回值 0。如果發生錯誤,比如要搜尋的檔案不存在,則返回大於 1 的值(GNU grep 總是返回 2)。
現在可以使用 grep
和基本的正規表示式構建塊了,以下是一些方便的快捷鍵。
- +
- + 操作符類似於 * 操作符,但是它匹配一次或多次前一個正規表示式。基本表示式中它必須轉義。
- ?
- ? 表示前一個表示式是可選的,因此它表示匹配 0 次或 1 次。這與萬用字元中使用的 ? 不同。
- .
- .(句點)是表示任何字元的元字元。最常使用的方式是 .*,該表示式匹配包含任何字元(或沒有字元)的任意長度的字串。不用說您就明白,這一般在一個較長的表示式中使用。比較句點與萬用字元中使用的 ?,.* 與萬用字元中使用的 *。
ian@attic4:~/lpi103-7$ grep "pp+" text1 # at least two p`s 1 apple ian@attic4:~/lpi103-7$ grep "pl?e" text1 1 apple 2 pear ian@attic4:~/lpi103-7$ grep "pl?e" text1 # pe with optional l between 1 apple 2 pear ian@attic4:~/lpi103-7$ grep "p.*r" text1 # p, some string then r 2 pear ian@attic4:~/lpi103-7$ grep "a.." text1 # a followed by two other letters 1 apple 3 banana |
^(脫字元號)匹配一行的開始,$(美元符號)匹配行的結束。^..b 匹配行開始處任何後跟 b 的兩個字元,而
ar$ 匹配任何以 ar 結束的行。正規表示式 ^$ 匹配空行。
到目前為止,我們已經學習了用於單個字元的重複。如果希望搜尋一個或多個多字元字串,比如 banan 中 an 出現了兩次,那麼可以使用圓括號,在基本語法中必須轉義。類似地,您可能希望搜尋一些字元,但又不想使用 . 這麼通用或者交替這麼囉嗦的表示式。那麼,您可以使用方括號([])將交替情況括起來,常規語法需要轉義。方括號中的表示式構成了一個字元類。使用方括號還可以減少轉義特殊字元(比如 . 和 *)的需求,例外情況見後文。
ian@attic4:~/lpi103-7$ grep "(an)+" text1 # find at least 1 an 3 banana ian@attic4:~/lpi103-7$ grep "an(an)+" text1 # find at least 2 an`s 3 banana ian@attic4:~/lpi103-7$ grep "[3p]" text1 # find p or 3 1 apple 2 pear 3 banana ian@attic4:~/lpi103-7$ echo -e "find an s* here somewhere." | grep "s[.*]" s* here ian@attic4:~/lpi103-7$ echo -e "find an * in position 2." | grep ".[.*]" * in position 2. |
字元類還有幾個有趣的可能性。
- 範圍表示式(Range expression)
- 範圍表示式是使用 -(連字元)分隔的雙字元,比如數字裡面的 0-9,十六進位制裡的 0-9a-fA-F。注意,範圍與語言環境有關。
- 署名類(Named class)
- 有些署名類可以為通常使用的類提供便捷。署名類以 [: 開始,以 :] 結束,可以在括號表示式中使用。示例如下:
- [:alnum:]
- 字母數字字元
- [:blank:]
- 空格和製表符
- [:digit:]
- 數字 0 到 9(等效於 0-9)
- [:upper:] 和 [:lower:]
- 分別為大寫字母和小寫字母。
- ^(求反)
- 在字元類 [ 後的第一個字元使用時,^(脫字元號)對剩餘字元求反,因此只有類中不存在該字元時(前導^除外)才能匹配。
瞭解了以上特殊含義後我們知道,如果希望匹配一個字元類中的字面值 -(連字元),那麼您必須將其放在第一個或最後一個。如果想匹配字面值^(脫字字元),那麼它不能是第一個字元。] 在非第一個位置時表示結束類。
字元類中,正規表示式和萬用字元是類似的,但使用的否定符號不同(^ 和 !)。清單 6 展示了一些字元類示例。
ian@attic4:~/lpi103-7$ # Match on range 3 through 7 ian@attic4:~/lpi103-7$ echo -e "123 456 789 0" | grep "[3-7]" 123 456 789 ian@attic4:~/lpi103-7$ # Find digit followed by no n or r till end of line ian@attic4:~/lpi103-7$ grep "[[:digit:]][^nr]*$" text1 1 apple ian@attic4:~/lpi103-7$ # Find a digit, n, or z followed by no n or r till end of line ian@attic4:~/lpi103-7$ grep "[[:digit:]nz][^nr]*$" text1 1 apple 3 banana |
最後一個示例讓您感到奇怪嗎? 在這種情況下,第一個括號表示式匹配字串中的任何數字、 n 或 z,至少 n 後面沒有另一個 n 或 r,因此字串結尾處的 na 匹配該正規表示式。
如果您能夠區分高亮顯示,比如用顏色、粗體或下劃線,那麼您可以設定 GREP_COLORS 環境變數來高亮顯示匹配內容。預設設定使用粗體紅色高亮顯示匹配內容,如圖 1 所示。您會看到整個輸出的第一行都是匹配的,但是第二行只匹配最後兩個字元。
如果您是正規表示式新手,或者不確定 grep 為什麼返回某一行,那麼這項技術可以幫您。
擴充套件的正規表示式語法是 GNU 擴充套件。我們在基本語法中使用時,它不需要轉義一些字元,包括圓括號、`?`、`+`、`|`和 `{`。但缺點在於,如果您在正規表示式中將它們作為字元解釋,那麼必須進行轉義。您可以使用
-E
(或者 grep 的 --extended-regexp
選項)表示您正在使用擴充套件的正規表示式語法。此外,egrep
命令也可以幫助您實現這一點。清單 7 展示了本節上文中使用的示例,以及
egrep
使用的相應擴充套件表示式。
ian@attic4:~/lpi103-7$ # Find b followed by one or more an`s and then an a ian@attic4:~/lpi103-7$ grep "b(an)+a" text1 3 banana ian@attic4:~/lpi103-7$ egrep "b(an)+a" text1 3 banana |
現在您瞭解了基本的命令,讓我們使用 grep
和 find
在檔案系統中查詢內容。示例相對比較簡單;它們使用
學習 Linux,101:文字流和過濾器 中建立的檔案或者您在 lpi103-7 目錄及其子目錄中建立的檔案。(參見 “設定示例”。)如果使用本系列之前的文章中建立的檔案,您將有一些額外的檔案,因此將看到一些額外的結果。
首先,grep
可以一次搜尋多個檔案。如果新增 -n
選項,它將告訴您匹配的行號。如果只想知道匹配多少行,可以使用
-c
選項,如果只想獲得匹配的檔案列表,可以使用 -l
選項。清單 8 展示了一些示例。
ian@attic4:~/lpi103-7$ grep plum * text2:9 plum yaa:9 plum ian@attic4:~/lpi103-7$ grep -n banana text[1-4] text1:3:3 banana text2:2:3 banana ian@attic4:~/lpi103-7$ grep -c banana text[1-4] text1:1 text2:1 text3:0 ian@attic4:~/lpi103-7$ grep -l pear * text1 text1.bkp xaa |
檢視清單 8 中的 -c
選項,您會看到一行 text3:0
。 您經常需要知道某個內容在檔案中出現了多少次,但是不用知道沒有出現該內容的檔案。grep
命令有一個
-v
選項,它表示只顯示不匹配的行輸出。因此,我們可以使用正規表示式 :0$
查詢以逗號和 0 結尾的行。
下一個示例是使用 find
查詢當前目錄及其子目錄中的所有常規檔案,然後使用 xargs
將檔案列表傳遞到
grep
,以確定每個檔案中出現 banana 的次數。最後,通過再一次呼叫 grep
篩選該輸出,這一次使用
-v
選項查詢所有不以 :0 結尾的行,只用告訴我們包含字串 banana 的檔案計數。
ian@attic4:~/lpi103-7$ find . -type f -print0| xargs -0 grep -c banana| grep -v ":0$" ./backup/text1.bkp.2:1 ./text2:1 ./text1:1 ./yaa:1 ./xab:1 ./text1.bkp:1 |
文章 “學習 Linux,101:文字流和過濾器” 中介紹了 sed——流編輯器,其中提到 sed 使用正規表示式。regexps 可以在地址表示式和替代表示式中使用。
如果您需要查詢某內容,那麼只需要使用 grep
。如果需要從匹配行中提取搜尋字串,或者相關字串,那麼需要進一步操作,您可以選擇使用
sed
。讓我們解釋一下它的工作方式。首先回憶我們的兩個示例檔案,text1 和 text2,其中包含了一個數字,後跟空格,再加一個水果的名稱,而 text3 包含重複的語句。我們在清單 10 中再看一次它的內容。
清單 10. text1、text2 和 text3 的內容
ian@attic4:~/lpi103-7$ cat text[1-3] 1 apple 2 pear 3 banana 9 plum 3 banana 10 apple This is a sentence. This is a sentence. This is a sentence. |
首先,我們將使用 grep
和 sed
提取以一個或多個數字開頭,且後跟空白字元(空格或製表符)的行。一般情況下,sed
在一個週期結束時列印出每個行,因此我們使用 sed 的
-n
選項禁止輸出,然後使用 sed
中的 p
命令只列印匹配我們正規表示式的行。要確認我們對這兩個工具使用的正規表示式相同,我們將其賦予一個變數。
ian@attic4:~/lpi103-7$ grep "$oursearch" text[1-3] text1:1 apple text1:2 pear text1:3 banana text2:9 plum text2:3 banana text2:10 apple ian@attic4:~/lpi103-7$ cat text[1-3] | sed -ne "/$oursearch/p" 1 apple 2 pear 3 banana 9 plum 3 banana 10 apple |
注意,grep
在搜尋到多個檔案時將顯示檔名稱。因為我們使用 cat
提供 sed
的輸出,所以
sed
無法知道原始檔名。但是,匹配行是相同的,正如我們期望的那樣。
現在假設我們只需要找到的行中的第二個字。在本例中是水果的名稱,但是我們需要查詢 HTTP URL 或者檔名等等其他內容。例如,刪除我們試圖匹配的字串就足夠了,如清單 12 所示。
ian@attic4:~/lpi103-7$ cat text[1-3] | sed -ne "/$oursearch/s/$oursearch//p" apple pear banana plum banana apple |
對於最後一個示例,假設我們的行在水果名稱之後還有些內容。我們新增了一行 “lemon pie”,檢視如何只提取 lemon。我們將對輸出排序,放棄非唯一的值,因此我們得到一個找到的水果列表,每個水果只出現一次。
清單 13 展示了兩種實現同一個任務的方式。首先,我們剔除了前導數字以及後面的空格,然後剔除第一個空格或選項卡之後的所有內容,並列印剩下的內容。在第二個示例中,我們引入了圓括號將整個行分為 3 個部分,數字和後面的空格、第二個字以及其他內容。我們使用
s
命令將整個行替換為第二個字,然後列印結果。您可以嘗試變化一下方式,忽略第三部分,(.*),看看是否能解釋發生了什麼。
ian@attic4:~/lpi103-7$ echo "7 lemon pie" | cat - text[1-3] | > sed -ne "/$oursearch/s/($oursearch)([^[:blank:]]*)(.*)/2/p" | > sort | uniq apple banana lemon pear |
有些舊版本的 sed
不支援擴充套件的正規表示式。如果您的 sed
版本不支援擴充套件的 regexps,請使用
-r
選項告訴 sed
您使用的是擴充套件語法。清單 14 展示了要對 oursearch
變數和
sed
命令進行哪些更改才能讓擴充套件的正規表示式完成清單 13 中基本正規表示式完成的任務。
ian@attic4:~/lpi103-7$ echo "7 lemon pie" | cat - text[1-3] | > sed -nre "/$oursearchx/s/($oursearchx)([^[:blank:]]*)(.*)/2/p" | > sort | uniq apple banana lemon pear plum |
本文介紹了您可以使用正規表示式以及 grep
和 sed
對 Linux 命令列執行的操作,這還只是冰山的一角。使用手冊瞭解更多有關這些高價值工具的資訊。
http://www.ibm.com/developerworks/cn/linux/l-lpic1-v3-103-7/
相關文章
- 6個使用正規表示式搜尋文字中內容的工具
- 使用MySQL之用正規表示式進行搜尋MySql
- 用正規表示式進行搜尋
- 正規表示式學習
- Linux中使用正規表示式進行文字匹配Linux
- 正規表示式的學習
- Oracle正規表示式學習Oracle
- 學習Java:正規表示式Java
- Go 正規表示式學習Go
- python 學習 -- 正規表示式的使用Python
- [MYSQL-9]用正規表示式進行搜尋MySql
- 匹配 XML 檔案正規表示式XML
- 匹配linux檔案路徑的正規表示式Linux
- 正規表示式學習和練習
- 正規表示式學習筆記筆記
- 正規表示式入門學習
- 如何快速學習正規表示式
- 正規表示式學習總結
- JavaScript學習1:正規表示式JavaScript
- python使用正規表示式文字替換Python
- 通過js正規表示式例項學習正規表示式基本語法JS
- 正規表示式例項蒐集,通過例項來學習正規表示式。
- 學習正規表示式(js、C#)JSC#
- java 正規表示式語法學習Java
- 你應該學習正規表示式
- Ruby學習筆記-正規表示式筆記
- 正規表示式學習筆記一筆記
- 簡單學習 JavaScript 正規表示式JavaScript
- PERL學習筆記---正規表示式筆記
- 正規表示式學習筆記 (轉)筆記
- 正規表示式學習(2)---字元特性字元
- Linux正規表示式使用指南Linux
- 【Linux】正規表示式Linux
- 正規表示式(初學)
- JavaScript正規表示式學習筆記(一)JavaScript筆記
- 從 Vue parseHTML 來學習正規表示式VueHTML
- Python學習筆記 - 正規表示式Python筆記
- js正規表示式基本語法學習JS