學習 Linux,101: 使用正則表達式搜索文本文件
來自: http://www.ibm.com/developerworks/cn/linux/l-lpic1-103-7/index.html?ca=drs-
概述
本教程將介紹使用正則表達式搜索文本文件的基礎 Linux 技術。學習如何:
- 創建簡單的正則表達式
- 使用正則表達式搜索文件和文件系統
- 結合使用正則表達式和 sed
本教程幫助您對 Linux Server Professional (LPIC-1) 考試 101 的主題 103 中的目標 103.7 進行應考準備。該目標的權重為 2。
回頁首
正則表達式
正則表達式起源于計算機語言理論。大多數計算機科學系的學生都知道,通過正則表達式表示的語言與通過有限自動機 (finite automata) 接受的語言完全相同。本教程中介紹的正則表達式能夠表達更高的復雜性,所以與您在計算機科學課程中學到的正則表達式可能 不同 ,但繼承關系很清楚。
關于本系列
本教程系列幫助學習 Linux 系統管理任務。您還可以使用這些教程中的資料對 Linux Professional Institute 的 LPIC-1:Linux 服務器專業認證考試 進行應考準備。
請參閱 學習 Linux,101 :LPIC-1 學習路線圖 ”,查看本系列中每部教程的描述和鏈接。這個路線圖正在開發之中,它反映了 2015 年 4 月 15 日更新的 4.0 版 LPIC-1 考試目標。在完成這些教程后,會將它們添加到路線圖中。
正則表達式 (也稱為 “regex” 或 “regexp”)是一種描述文本字符串或 模式 的方式,這樣程序就可以將該模式與任意文本字符串 相匹配 ,提供非常強大的搜索能力。 grep (表示 g eneralized r egular e xpression p rocessor,通用正則表達式處理器)是任何 Linux 或 UNIX?程序員或管理員工具箱中的標配,可以幫助您在文件搜索或命令輸出中使用正則表達式。在教程 “ 學習 Linux 101:文本流和過濾器 ” 中,我們介紹了 sed ( s tream ed itor,流編輯器),它是另一個廣泛使用正則表達式來查找和替換文件或文本流中的文本的標準工具。本教程將幫助您更好地理解 grep 和 sed 使用的正則表達式。另一個廣泛使用正則表達式的程序是 awk 。
與本教程系列的其他部分一樣,正則表達式和計算機語言理論足夠寫一本書。請參見參考資料來獲得一些建議。
在學習正則表達式后,您將看到正則表達式語法與教程 “ 學習 Linux 101:文件和目錄管理 ” 中討論的通配符(或 globbing)語法之間的一些相似性。這些相似性只是表面上的。
前提條件
要從本系列教程中獲得最大收獲,您應該掌握 Linux 的基本知識和一個正常工作的 Linux 系統,您可以在這個系統上實踐本教程中涵蓋的命令。有時程序的不同版本將得到不同的輸出格式,所以您的結果可能并不總是與這里給出的清單和圖完全相同。這里給出的示例中的結果是在 Ubuntu 15.04 發行版上獲得的。本教程建立在之前的教程 “ 學習 Linux 101:文本流和過濾器 ” 中討論的概念之上。
回頁首
設置示例
在本教程中,我們將使用教程 “ 學習 Linux 101:文本流和過濾器 ” 中創建的一些文件來練習命令。如果還沒有創建這些文件或沒有保存您使用的文件,可以首先在您的主目錄 lpi103-7 中創建一個新子目錄,然后在其中創建必要的文件。為此,可以打開一個文本窗口,使用您的主目錄作為當前目錄。然后將 清單 1的內容復制到窗口中,以便運行創建 lpi103-7 子目錄和您將使用的文件的命令。 提示: 在大多數 X 視窗系統中,按住鼠標中鍵會將選定的文本粘貼在光標位置。選定的文本可以位于同一個或另一個窗口中。
清單 1. 創建示例文件
mkdir -p lpi103-7 && cd lpi103-7 && { echo -e "1 apple\n2 pear\n3 banana" > text1 echo -e "9\tplum\n3\tbanana\n10\tapple" > 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 目錄。
清單 2. 創建示例文件 -- 輸出
點擊查看代碼清單
關閉 [x]
清單 2. 創建示例文件 -- 輸出
ian@yoga-u15:~$ mkdir -p lpi103-7 && cd lpi103-7 && { > echo -e "1 apple\n2 pear\n3 banana" > text1 > echo -e "9\tplum\n3\tbanana\n10\tapple" > text2 > echo "This is a sentence. " !#:* !#:1->text3echo "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@yoga-u15:~/lpi103-7$
</div>
回頁首
正則表達式構建塊
大多數 Linux 系統上提供的 GNU grep 程序都使用了以下兩種形式的正則表達式語法: 基本 和 擴展 語法。對于 GNU grep,在功能上沒什么不同。這里將介紹基本語法,以及它與擴展語法之間的區別。
正則表達式是從 字符 和 運算符 構建的,并通過 元字符 來擴充。大部分字符都匹配自己,大部分元字符必須使用反斜杠 (\) 來轉義。基本操作是:
- 串聯
- 串聯兩個正則表達式來創建一個更長的表達式。例如,正則表達式 a 將匹配字符串 abcdcba 兩次(第一個和最后一個 a ),正則表達式 b 也是如此。但是, ab 僅匹配 ab cdcba, ba 僅匹配 abcdc ba 。
- 重復
- 克萊尼星號 * 或重復運算符將匹配前一個正則表達式 0 次或多次。因此,像 a*b 這樣的表達式將匹配任何在 a 后以 b 結尾的字符串,包括 b 本身(這是 0 個 a 后跟 b 的字符串)。克萊尼星號 * 不需要轉義,所以,想要匹配一個文字星號 (*) 的任何表達式都必須將星號轉義。這里的 * 的用法不同于通配符中的用法,后者可以匹配任何字符串。
- 交替
- 交替運算符 (|) 匹配前一個或后一個的表達式。它在基本語法中必須轉義。例如表達式 a*\|b*c 將匹配一個包含任意多個 a 或包含任意多個 b (但不能同時包含二者)且以一個 c 結尾的字符串。同樣地,它將匹配單字符 c 。
您通常需要引用正則表達式來避免 shell 擴展。
回頁首
搜索文件和文件系統
我們將使用之前創建的文本文件作為示例(參閱 “設置示例”)。分析 清單 3中的簡單示例。請注意, grep 獲取一個正則表達式作為必需參數,并包含要搜索的 0 或多個文件的列表。如果未提供文件,grep 將會搜索 stdin,這使它成為一個可以在管道中使用的過濾器。如果任何行都不匹配,則 grep 沒有輸出,但可以測試它的退出代碼。
清單 3. 簡單的正則表達式
ian@yoga-u15:~/lpi103-7$ grep p text1 1 apple 2 pear ian@yoga-u15:~/lpi103-7$ grep pea text1 2 pear ian@yoga-u15:~/lpi103-7$ grep "p*" text1 1 apple 2 pear 3 banana ian@yoga-u15:~/lpi103-7$ grep "pp*" text1 1 apple 2 pear ian@yoga-u15:~/lpi103-7$ grep "x" text1; echo $? 1 ian@yoga-u15:~/lpi103-7$ grep "x*" text1; echo $? 1 apple 2 pear 3 banana 0 ian@yoga-u15:~/lpi103-7$ cat text1 | grep "l\|n" 1 apple 3 banana ian@yoga-u15:~/lpi103-7$ echo -e "find an \ns* here" | grep "s\*"
從這些示例可以看到,您有時會獲得出乎意料的結果,尤其在使用重復運算符時。您可能預計 p* 或者至少 pp* 與兩個 p 匹配,但 p* 和 x* 匹配文件中的每一行,因為 * 運算符匹配前一個正則表達式 0 次或多次。
兩個示例演示了 grep 的退出代碼。如果找到一個匹配值,則返回值 0,如果未找到匹配值,則返回值 1。發生錯誤時,可能會返回大于 1 的值(GNU grep 始終為 2),比如您嘗試搜索的文件不存在。
第一篇快捷鍵
現在您已經可以將正則表達式的基本構建塊用于 grep ,這是一些方便的快捷鍵。
- +
- + 運算符就像 * 運算符一樣,但它匹配前一個正則表達式的 一次 或多次出現。它在基本表達式中必須轉義。
- ?
- ? 表示前一個表達式是可選的,因此它表示匹配 0 次或 1 次。這與通配符中使用的 ? 不同。
- .
- .(句點)是一個表示任何字符的元字符。最常用的一種模式是 .* ,它匹配包含任意字符(或完全不含字符)的任意長度的字符串。不用說您就已經明白,它會用在較長的表達式中。比較句點與通配符中使用的 ?,以及 .* 與通配符中使用的 *。
清單 4. 更多正則表達式
ian@yoga-u15:~/lpi103-7$ grep "pp\+" text1 # at least two p's 1 apple ian@yoga-u15:~/lpi103-7$ grep "pl\?e" text1 1 apple 2 pear ian@yoga-u15:~/lpi103-7$ grep "pl\?e" text1 # pe with optional l between 1 apple 2 pear ian@yoga-u15:~/lpi103-7$ grep "p.*r" text1 # p, some string then r 2 pear ian@yoga-u15:~/lpi103-7$ grep "a.." text1 # a followed by two other letters 1 apple 3 banana
匹配一行的開頭或末尾
^(脫字符)匹配一行的開頭,而 $(美元符號)匹配一行的末尾。所以 ^..b 匹配一行開頭任何后跟 b 的兩個字符,而 ar$ 匹配任何以 ar 結尾的行。正則表達式 ^$ 匹配任何空行。
更復雜的表達式
目前我們已看到了應用于單個字符的重復運算符。如果您想搜索一個多字符串的一個或多個出現位置,比如 b anan a 中出現了兩次的 an ,可使用圓括號,圓括號在基本語法中必須轉義。類似地,您可能希望搜索一些字符,但不使用 . 這么通用的或交替這么冗長的運算符。可以將交替表達式放在方括號中 ([]),這樣它們就不會針對常規語法而進行轉義。方括號中的表達式構成了一個 字符類 。除了稍后介紹的一些例外情況之外,使用方括號還可以消除轉義 . 和 * 等特殊字符的需要。
清單 5. 圓括號和字符類
ian@yoga-u15:~/lpi103-7$ grep "\(an\)\+" text1 # find at least 1 an 3 banana ian@yoga-u15:~/lpi103-7$ grep "an\(an\)\+" text1 # find at least 2 an's 3 banana ian@yoga-u15:~/lpi103-7$ grep "[3p]" text1 # find p or 3 1 apple 2 pear 3 banana ian@yoga-u15:~/lpi103-7$ echo -e "find an\ns* here\nsomewhere." | grep "s[.*]" s* here ian@yoga-u15:~/lpi103-7$ echo -e "find a\n . or * in position 2." | grep "^.[.*]" . or * in position 2.
字符類還有其他一些有趣的可能用法。
- 范圍表達式
- 范圍表達式是兩個由 -(連字符)分開的字符,比如 0-9 表示數字,或者 0-9a-fA-F 表示十六進制數。請注意范圍依賴于語言環境。
- 命名類
- 一些命名類提供了常用類的方便的簡略表達方式。命名類以 [: 開頭并以 :] 結束,可用在括號表達式內。一些例子:
- [:alnum:]
- 字母數字字符。
- [:blank:]
- 空格和制表符字符。
- [:digit:]
- 數字 0 到 9(等效于 0-9)。
- [:upper:] 和 [:lower:]
- 分別表示大寫和小寫字母。
考慮到上面的特殊含義,如果想要匹配一個字符類中的文字 -(連字符),必須將它放在開頭或末尾處。如果想要匹配文字 ^(脫字符),那么不能讓它成為第一個字符。](右方括號)結束該類,除非它放在第一位。
字符類是正則表達式和通配符比較 相似 的一個地方,但求反不同(^ 與 .!)。清單 6給出了字符類的一些示例。
清單 6. 更多字符類
ian@yoga-u15:~/lpi103-7$ # Match on range 3 through 7 ian@yoga-u15:~/lpi103-7$ echo -e "123\n456\n789\n0" | grep "[3-7]" 123 456 789 ian@yoga-u15:~/lpi103-7$ # Find digit followed by no n or r till end of line ian@yoga-u15:~/lpi103-7$ grep "[[:digit:]][^nr]*$" text1 1 apple ian@yoga-u15:~/lpi103-7$ # Find a digit, n, or z followed by no n or r till end of line ian@yoga-u15:~/lpi103-7$ grep "[[:digit:]nz][^nr]*$" text1 1 apple 3 banana
最后一個示例是否出乎您的意料?在這種情況下,第一個括號表達式匹配字符串中的 任何 數字,n 或 z,最后一個 n 后沒有另一個 n 或 r,所以字符串末尾的 na 與該正則表達式相匹配。
哪些內容匹配?
如果您能夠區分突出顯示,比如顏色、加粗或下劃線,那么您可以設置 GREP_COLORS 環境變量來突出顯示非空匹配。默認設置以加粗的紅色來突出顯示匹配,如 圖 1中所示。可以看到輸出中的整個第一行都匹配,但第二行中只有最后兩個字符匹配。
圖 1. 為 grep 匹配使用顏色
如果您不熟悉正則表達式,或者不確定為什么 grep 返回某個特定的行,此技術可以幫助您。
還可以使用 -o 或 grep 的 --only-matching 選項來僅顯示(非空)匹配 ,其中每個匹配值放在單獨一個輸出行上,如 清單 7中所示。
清單 7. 使用 grep 和 -o 選項顯示匹配
ian@yoga-u15:~/lpi103-7$ grep -o "[[:digit:]nz][^nr]*$" text1 1 apple na
回頁首
擴展的正則表達式
擴展的正則表達式語法是一種 GNU 擴展。它消除了像在基本語法中使用時一樣轉義一些字符的需要,包括圓括號、‘ ? ’、‘ + ’、‘ | ’ 和 ‘ { ’。缺點是如果您想在正則表達式中將它們解釋為字符,則必須轉義它們。您可以使用 -E (或 grep 的 --extended-regexp 選項)來指定您在使用擴展的正則表達式語法。 egrep 命令也能為您實現此目的。 清單 8顯示了本節前面使用的一個示例和使用 egrep 的相應的擴展表達式。
清單 8. 擴展的正則表達式
ian@yoga-u15:~/lpi103-7$ # Find b followed by one or more an's and then an a ian@yoga-u15:~/lpi103-7$ grep "b\(an\)\+a" text1 3 banana ian@yoga-u15:~/lpi103-7$ egrep "b(an)+a" text1 3 banana
回頁首
在文件中查找內容
現在您已經擁有基本語法中的一個命令,讓我們使用 grep 和 find 的強大功能在文件系統中查找內容。這些示例也比較簡單,它們使用了之前的一篇教程中創建的文件或您在 lpi103-7 目錄及其子目錄中創建的文件。(參閱 “設置示例。”)如果使用來自本系列之前的教程的文件,您將擁有一些額外的文件,而且將會看到一些額外的結果。
首先, grep 可一次搜索多個文件。如果添加 -n 選項,它會告訴您哪些行號匹配。如果只想知道有多少行匹配,可以使用 -c 選項,如果只想要包含匹配內容的文件列表,可以使用 -l 選項。 清單 9顯示了一些示例。
清單 9. 搜索多個文件
ian@yoga-u15:~/lpi103-7$ grep plum * grep: backup: Is a directory text2:9 plum yaa:9 plum ian@yoga-u15:~/lpi103-7$ grep -n banana text[1-4] text1:3:3 banana text2:2:3 banana ian@yoga-u15:~/lpi103-7$ grep -c banana text[1-4] text1:1 text2:1 text3:0 ian@yoga-u15:~/lpi103-7$ grep -l pear * grep: backup: Is a directory text1 text1.bkp xaa ian@yoga-u15:~/lpi103-7$ echo $? # Error code because backup is a directory 2
如果查看 清單 9中的 -c 選項的用法,您會看到一個 text3:0 行。您通常想要知道某個內容在一個文件中出現了多少次,而不是想知道哪些文件沒有您查找的內容。 grep 命令有一個 -v 選項,該選項告訴它僅顯示 不 匹配的行的輸出。所以我們可以結合使用 -v 和正則表達式 :0$ 來查找 未 以冒號和 0 結尾的行。
我們的下一個示例實現此目的的方式是,使用 find 找到當前目錄及其子目錄中的所有正則文件,然后使用 xargs 將文件列表傳遞給 grep 來確定 banana 在每個文件中出現的次數。最后,通過另一次調用 grep 來過濾此輸出,這一次使用 -v 選項查找所有 未 以 :0 結尾的行,僅留下實際包含字符串 banana 的文件數量。
清單 10. 查找包含至少一個 banana 的文件
ian@yoga-u15:~/lpi103-7$ find . -type f -print0| xargs -0 grep -c banana| grep -v ":0$" ./text1:1 ./yaa:1 ./backup/text1.bkp.2:1 ./text2:1 ./text1.bkp:1 ./xab:1
回頁首
正則表達式和 sed
教程 “ 學習 Linux 101:文本流和過濾器 ” 中對 sed(流編輯器)的介紹提到 sed 使用了正則表達式。Regexps 可用在地址表達式和替換表達式中。
如果僅查找某個內容,可以只使用 grep 。如果需要從匹配的行提取搜索字符串或一個相關的字符串,然后進一步處理它,您可以選擇 sed 。讓我們看看它的工作原理。首先回想一下,我們的兩個示例文件 text1 和 text2 包含一個數字后跟空格,然后是一個水果名稱,而 text3 包含一個重復的句子。我們在 清單 11中再次給出了這些內容。
清單 11. text1、text2 和 text3 的內容
ian@yoga-u15:~/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 命令只打印與我們的正則表達式匹配的行。為了確認我們對兩種工具使用了同一個正則表達式,我們將它賦給一個變量。
清單 12. 使用 grep 和 sed 搜索
ian@yoga-u15:~/lpi103-7$ oursearch='^[[:digit:]][[:digit:]]*[[:blank:]]' ian@yoga-u15:~/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@yoga-u15:~/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 或文件名,或查找幾乎其他任何信息。對于我們的示例,準確刪除我們嘗試匹配的字符串就足夠了,如 清單 13中所示。
清單 13. 使用 sed 刪除前導數字
ian@yoga-u15:~/lpi103-7$ cat text[1-3] | sed -ne "/$oursearch/s/$oursearch//p" apple pear banana plum banana apple
對于我們的最后一個示例,假設我們的行可能在水果名稱后包含一些內容。我們將向內容中添加一個顯示為 “lemon pie” 的行,看看如何僅獲取 lemon。我們還將對輸出進行排序并丟棄非唯一值,所以我們獲得了找到的水果列表,每種水果僅出現一次。
清單 14顯示了完成同一個任務的兩種方式。第一個示例是,我們首先剔除前導數字和它之后的空格,然后通過 sed 剔除第一個空格或制表符后的任何內容,并打印剩余內容。在第二個示例中,我們引入了圓括號來將整行分解為 3 部分:數字和后面的空格、第二個單詞,以及其他任何內容。我們使用 s 命令將整行替換為第二個單詞,然后打印結果。您可能想嘗試一種不同的方法,省略第三部分 \(.*\),看看您能否解釋發生的情況。
清單 14. Getting to the core of the fruit
ian@yoga-u15:~/lpi103-7$ $ echo "7 lemon pie" | cat - text[1-3] | > sed -ne "/$oursearch/s/\($oursearch\)//p" | > sed "s/[[:blank:]].*//" | > sort | uniq $: command not found apple banana pear plum 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 不支持擴展的正則表達式,可以使用 -r 選項告訴 sed 您在使用擴展的語法。 清單 15展示了需要對 oursearch 變量和 sed 命令執行哪些更改,才能使用擴展的正則表達式完成 清單 14中使用基本正則表達式完成的任務。
清單 15. 結合使用擴展的正則表達式和 sed
ian@yoga-u15:~/lpi103-7$ oursearchx='^[[:digit:]]+[[:blank:]]' ian@yoga-u15:~/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 命令行上執行的操作。使用手冊頁了解這些寶貴工具的更多信息。
</div>