第十九章 正則表達式
文本型數據在所有的類UNIX系統(如 Linux)中會扮演著重要角色,在完全領會這些工具的全部特征之前,要先了解一下工具最為復雜的用法和相關技術:正則表達式。
什么是正則表達式
簡單地說,正則表達式是一種用于識別文本模式的符號表示法,在某種程度上類似于匹配文件和路徑名的Shell通配符,但用途更廣。大多數工具和編程語言都支持正則表達式,以便于解決文本問題。正則表達式并非全都相同;不同的工具,不同的編程語言,其正則表達式實現略有差異。本章正則表達式限定在POSIX標準范圍內,涵蓋了大多數命令行工具,相較于許多編程語言,POSIX使用的符號寫法要略微豐富一些。
grep
處理正則表達式的主要命令是grep。grep源于global regular expression print, 譯為全局正則表達式輸出。grep的基本功能實在文本文件搜索與指定的正則表達式匹配的文本,將包含匹配項的文本輸出到標準輸出。
grep命令用法如下,其中regex代表正則表達式:
grep [options] regex [file...]
常用grep選項
選項 | 描述 |
---|---|
-i, --ignore-case | 忽略字母大小寫。不區分大寫字母和小寫字母 |
-v, --invert-match | 反向匹配。在正常情況下,grep會輸出包含匹配項的文本行。該選項則使grep輸出所有不包含匹配項的文本行 |
-c, --count | 輸出匹配數量(如果同時制定了-v選項,則不輸出匹配數量),不在輸出文本行 |
-l, --files-with-matches | 輸出包含匹配項的文件名,不在輸出文本行 |
-L,–files-without-match | 和-l選項類似,但是只輸出不包含匹配的文件名 |
-n,–line-number | 在包含匹配項的文本行之前加上行號 |
-h,–no-filename | 在多文件搜索中禁止輸出文件名 |
例如:在以dirlist開頭的txt文件內搜索bzip字符串
grep bzip dirlist*.txt
命令輸出如下:
dirlist-bin.txt:bzip2
dirlist-bin.txt:bzip2recover
如果只需要包含匹配的項的文件,并不需要匹配項,可以指定-l選項:
grep -l bzip dirlist*.txt
輸出結果如下:
dirlist-bin.txt
如果只需要不包含匹配項的文件,可以這樣做:
grep -L bzip dirlist*.txt
輸出結果如下:
dirlist-sbin.txt
dirlist-usr-bin.txt
dirlist-usr-sbin.txt
元字符與文字字符
通過grep搜索的時候其實已經正在使用正則表達式了,盡管是非常簡單的那種。正則表達式bzip的意思是僅當文件中的某行至少包含4個字符且字符順序為b、z、i、p的時候(之間沒有任何其他字符)才匹配。字符串bzp中的字符全部都是文字字符(literal character),只能匹配自身。除了普通字符,正則表達式還包括元字符(metacharacter),用于指定更復雜的匹配。正則表達式元字符包括:
^ $ . [ ] - ? * + ( ) | \
其他所有字符均被視為普通字符,不過在少數情況下,反斜線字符可用于創建元序列(metasequenece),還能轉義字符,使其成為普通字符。
注意
很多正則表達式元字符對Shell擴展具有特殊含義。當包含元字符的正則表達式出現在命令行上時,一定要記得將其放入引號中,避免Shell去擴展這些字符,這一點非常重要。
任意字符
用于匹配任意字符的元字符是點號“.”,它可用于匹配任意字符。如果我們將其放入正則表達式,它能夠匹配該字符位置上的任意字符。
grep -h '.zip' dirlist*.txt
輸出結果如下:
bunzip2
bzip2
bzip2recover
gunzip
gzip
funzip
gpg-zip
preunzip
prezip
prezip-bin
unzip
unzipfx
在文件內搜索匹配正則表達式.zip的所有行。最終結果的有些地方值得注意。首先,在其中沒有發現.zip程序。這是因為正則表達式中的點號元字符將需要匹配的字符串長度增加到了4個字符,又因為zip只包含3個字符,所以不匹配。如果文件列表中有擴展名為.zip的文件,也能夠匹配,因為擴展中的點好也屬于“任意字符”的范疇。
錨點
在正則表達式中,脫字符^和美元符號$被視為錨點(anchor),分別表示僅當正則表達式出現在行首或行尾的時候才匹配。
例如在以dirlist開頭的txt文件中匹配行首為zip的行:
grep -h '^zip' dirlist*.txt
命令輸出結果如下:
zip
zipcloak
zipgrep
zipinfo
zipnote
zipsplit
在以dirlist開頭的txt文件中匹配行尾為zip的行:
grep -h 'zip$' dirlist*.txt
命令行輸出結果如下:
gunzip
gzip
funzip
gpg-zip
preunzip
prezip
unzip
zip
在以dirlist開頭的txt文件匹配行內容為zip文件:
grep -h '^zip$' dirlist*.txt
該命令輸出結果如下:
zip
在文件列表中分別搜索于行首、位于行尾、單獨作為一行的字符串zip。注意,正則表達式^$(表示行首和行尾之間什么都沒有)可以匹配空行。
方括號表達式與字符類
除了匹配正則表達式中指定位置上的任意字符,還可以使用方括號表達式來匹配指定字符集合中的單個字符。借助方括號表達式,可以指定一組待匹配的字符(包括會被解釋為元字符的字符)。使用兩個字符組成的集合來匹配包含字符串bzip或gzip的行:
grep -h '[bg]zip' dirlist*.txt
集合中可以包含任意數量的字符,其中出現的元字符會丟失其特殊含義。但是,有兩種特殊情況:脫字符用于表示否定;連字符表示字符范圍。
排除
如果方括號表達式中的首個字符是脫字符,剩下的字符則被視為不該在指定字符位置出現的字符集合。將前一個例子修改如下:
grep -h '[^bg]zip' dirlist*.txt
輸出結果如下:
bunzip2
gunzip
funzip
gpg-zip
preunzip
prezip
prezip-bin
unzip
unzipsfx
利用排除操作,得到了一份文件列表,其中的文件名均包含字符串zip,而該字符之前是除b或g之外的任意字符。注意,zip并不符合搜索文件。排除型字符集合仍需要指定位置上有一個字符存在,只不過這個字符不能使集合中的字符。
僅當脫字符是方括號表達式中的第一個字符的時候才表示排除含義;否則,它只代表一個普通字符。
傳統的字符范圍
構建一個正則表達式,查找文件列表中所有以大寫字母開頭的文件,可以這樣做:
grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' dirlist*.txt
把26個大寫字母放進方括號表達式里就能搞定的事。但這種方法比較實在麻煩,另一種做法:
grep -h '^[A-Z]' dirlist*.txt
輸出結果如下:
MAKEDEV
ControlPanel
GET
HEAD
POST
X
X11
Xorg
MAKEFLOPPIES
NetworkManager
NetworkManagerDisaptcher
通過使用3個字符表示的字符范圍,直接實現了26個字母的縮寫。不管哪種字符范圍,都可以用這種方式表達,例如下面的正則表達式可以匹配以字母或數字開頭的所有文件名:
grep -h '^[A-Za-z0-9]' dirlist*.txt
如何在方括號表達式中加入一個普通的連字符,這將匹配包含大寫字母的文件名,以下文件將匹配每個包含破折號或大寫A(Z)的文件名。
grep -h '[-AZ]' dirlist*.txt
POSIX字符類
傳統的字符范圍易于理解,能夠有效地解決快速指定字符集合的問題。遺憾的是,這種方法未必總是管用。在使用grep時沒有出現什么問題,很難說不會在其他程序那里遇到問題。
字符范圍的用法幾乎與正則表達式中的一致,但有個問題:
ls /usr/sbin/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]
該命令輸出結果如下:
/usr/sbin/ModemManager
/usr/sbin/NetworkManager
取決于Linux發行版,得到的文件列表也不盡相同,也有可能時空列表。本例取自Ubuntu。該命令的結果符合預期——以大寫字母開頭的文件列表,但下面的命令得到的結果就完全不同了(只顯示其中一部分):
ls /usr/sbin/[A-Z]*
該命令輸出結果如下:
/usr/sbin/biosdecode
/usr/sbin/chat
/usr/sbin/chgpasswd
/usr/sbin/chpasswd
/usr/sbin/chroot
/usr/sbin/cleanup-info
/usr/sbin/complain
/usr/sbin/console-kit-daemon
該例子沒有按照預期輸出,這是因為UNIX開發支出只識別ASCCII字符,其排序規則如下(collation order):
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
這與正常的詞典序不同,后者如下:
aAbBcCdDeEfFgGhHiIJkKlLmMnNoOpPQrRsStTuUvVwWxXyYzZ
隨著UNIX的流行,支持美式英語字符以外的字符的需求也與日俱增。于是ASCII長度擴展到8bit,添加了編碼值128~255字符,適應了更多的語言。因此POSIX標準引入了語言環境(locale)的概念,能夠通過調整來選擇特定區域所需要的字符集。可以使用以下命令查看系統的語言配置:
echo $LANG
en_US.UTF-8
隨著UNIX的流行,兼容POSIX的應用就會使用詞典排序,而不再是ASCII的順序。這就解釋了上述命令結果的不同。在詞典序中,字符范圍[A-Z]包含除a之外的所有字母,這也正是看到的結果。
為了部分解決這個問題,POSIX標準包含了許多字符類(character class),提供了各種有用的字符范圍。
POSIX字符類
字符類 | 描述 |
---|---|
[:alnum:] | 字母和數字字符。在ASCII中,等價于[a-zA-Z0-9] |
[:word:] | 和[:alnum:]一樣,另外加入了下畫線字符_ |
[;alpha;] | 字母字符。在ASCII中,等價于[a-zA-Z] |
[:blank:] | 包括空格符和制表符 |
[:cntrl:] | ASCII控制字符。包括ASCII編碼值0~31和127的字符 |
[:digit:] | 數字0~9 |
[:graph:] | 可見字符。在ASCII中,包括編碼值為33~126的字符 |
[:lower:] | 小寫字母 |
[:punct:] | 標點符號字符。在ASCII中,等價于[-!"#$%&()*+,./:;<=>?@[\]_`{ |
[:print:] | 可輸出字符。包括[:graph:]中的所有字符加上空格符 |
[:space:] | 空白字符,包括空格符、制表符、回車符、換行符、垂直制表符、換頁符 |
[:upper:] | 大寫字符 |
[:xdigit;] | 用于表示十六進制數值的字符。在ASCII中,等價于[0-9A-Fa-f] |
即便有了POSIX字符類,還是沒有便利的方法來表示部分范圍,例如[A-M]。 |
這不是正則表達式的示例,而是Shell路徑名擴展的示例。我們之所以在此演示,是因為POSIX字符類既可用于正則表達式,也可用于Shell擴展。
POSIX基本型正則表達式于擴展型正則表達式
POSIX把正則表達式的實現分為了兩類:基本正則表達式(Basic Regular Expression, BRE)與擴展正則表達式(Extended Regular Expression, ERE)。所有兼容POSIX并實現了BRE應用程序都支持目前介紹的這些特征。grep程序就是這樣的程序之一。
BRE和ERE的不同在于元字符。BRE識別下列元字符:
^ $ . [ ] *
除此之外的所有字符均被視為文字字符。ERE又加入了下列元字符(及其功能):
( ) { } ? + |
但是,如果使用了反斜線將(、)、{、}轉義的話,BRE將其視為元字符;而BRE會將轉義后的這些字符視為文字字符。
若要使用ERE,就得使用另一種grep。傳統上,這要借助于egrep程序,但是GNU版本的grep程序可以使用-E選項來支持ERE。
POSIX:電氣電子工程師學會(Institute of Electrical and Electronics Engineer, IEEE)出現了。IEEE制定了一套規范UNIX(以及類UNIX系統)
工作方式標準。這些標準官方名稱是IEEE 1003,它定義了應用程序接口(Application Programming Interface, API)、Shell以及標準類UNIX系統中的實用工具。POSIX這個名字是由理查地·馬修·斯托曼建議,結尾增加的X只是為了讓名字更響亮,該叫法后被IEEE采納。
多選結構
ERE的特征叫作多選結構(alternation),它允許匹配一組正則表達式中的某一個。就像方括號表達式允許匹配一組指定字符中的單個字符,多選結構可以從一組字符串或正則表達式中尋找匹配。
例如:查找包含AAA或BBB內容的行,其命令如下:
grep -E 'AAA | BBB'
其中-E選項指定采用ERE(擴展性正則表達式),將正則表達式放入引號中,避免Shell將其解釋為管道。多選結構可不僅能二選一:
grep -E 'AAA | BBB | CCC'
將多選結構與其它正則表達式元素組合起來,可以實用()來分隔,例如:
grep -Eh '^(bz | gz | zip)' dirlist*.txt
改正則表達式可以匹配文件列表中以bz、gz或zip開頭的文件名。如果去掉括號,正則表達式的含義就變成了匹配以bz開頭,或者包含gz,或者包含zip的文件名:
grep -Eh '^bz | gz | zip' dirlist*.txt
量詞
ERE支持多種方式指定匹配次數
?——匹配0次或1次
實際上,該量詞(quantifier)表示“之前的元素是可選的”。假設我們要檢查電話號碼有效性。電話號碼匹配下列兩種形式之一(n為數字),則認為是有效的。
- (nnn)nnn-nnnn。
- nnn nnn-nnnn
根據據此構建下列正則表達式:
^(?[0-9] [0-9] [0-9] )? [0-9] [0-9] [0-9] - [0-9] [0-9] [0-9] [0-9]$
其中,在括號后加上了問號,表示匹配括號的內容0次或1次。因為括號是元字符(在ERE中),所以在其之前加上反斜線,使其成為文字字符。
匹配符合(nnn)nnn-nnnn格式的電話號碼:
echo "(555) 123-4567" | grep -E '^\(?[0-9] [0-9] [0-9]\)? [0-9] [0-9] [0-9] - [0-9] [0-9] [0-9] [0-9]$'
命令輸出如下:
(555) 123-4567
從結果可以看出能夠匹配nnn nnn-nnnn格式的電話號碼。
匹配符合nnn-nnnn格式的電話號碼:
echo "555 123-4567" | grep -E '^\(? [0-9] [0-9] [0-9]\)? [0-9] [0-9] [0-9] - [0-9] [0-9] [0-9] [0-9]$'
命令輸出如下:
555 123-4567
可以看到能夠匹配符合nnn nnn-nnnn格式的電話號碼。
匹配不符合以上兩種格式的情況:
echo "AAA 123-4567" | grep -E '^\(? [0-9] [0-9] [0-9]\)? [0-9] [0-9] [0-9] - [0-9] [0-9] [0-9] [0-9] $'
該命令無輸出表示不符合以上兩種格式,說明該正則表達式可以用于初步驗證電話號碼。
*——匹配0次或多次
和?一樣,*也可用于表示可選項;但和?不同的是,*之前的可選項可以出現任意多次,而不僅僅出現一次。例如判斷某個字符串是否是一句話;也就是說該字符以一個大寫字母開頭,然后是任意多個大/小寫字母和空格符,最后以點好結尾。可以實用下列正則表達式:
[ [:upper:] ] [ [;upper;] [;lower;] ]* .
這個正則表達式由3項組成:包含字符類[:upper:]的方括號表達式,包含字符類[;upper;]、[;lower;]以及空格符的方括號表達式,經過反斜線轉義的點好。第二項結尾處是*,所以在句子開頭的大寫字母之后,不管有多少個大/小寫字母和空格符,都能夠匹配。
匹配只有首字母大寫的句子:
echo "This works." | grep -E '[ [:upper:] ] [ [;upper;] [;lower;] ]* \.'
命令輸出如下:
This works
從結果可以匹配只有首字母大寫的句子。
匹配首字母大寫并且除此之外還有其他大寫字母的句子:
echo "This Works" | grep -E '[ [:upper:] ] [ [;upper;] [;lower;] ]* \.'
輸出結果如下:
This Works
從結果可以看出能夠匹配首字母大寫并且含有其他大寫字母的句子。
匹配只有小寫字母的句子:
echo "this works not" | grep -E '[ [:upper:] ] [ [;upper;] [;lower;] ]* \.'
沒有輸出結果,說明該正則表達式正確,因為首字母不是大寫字母。
+——匹配1次或多次
+和*差不多,只不過要求之前的可選項至少匹配一次。下面的正則表達式所匹配的行只能包含由單個空格分隔的一個或多個字母:
^ ( [ [:alpha:] ] + ?) + $
匹配有兩個單詞和一個空格分隔符:
echo "This that" | grep -E '^([[:alpha:]]+ ?)+$'
輸出結果如下:
This that
可以看出能夠匹配兩個單詞一個空白分隔符。
匹配3個字母每兩個字母間隔一個空格分隔符
echo "a b c" | grep -E '^([[:alpha:]]+ ?)+ $'
輸出結果如下:
a b c
從結果可看出可以匹配由單個字母和空白分隔符組成的句子。
匹配由字母和數字匹配單個字母和空白分隔符組成的句子:
echo "a b 9" | grep -E '^([[;alpha;]]+ ?)+$'
沒有輸出結果,因為不能包含非字母字符。
匹配由單詞和字母以及兩個空白分隔符組成的句子:
echo "abc d" | grep -E '^([[:alpha:]]+ ?)+$'
沒有輸出結果,因為c和d之間不止有一個空白字符。
{} —— 匹配指定次數
{和}用于指定要求匹配的最小次數和最大次數,共有4種指定方式。
指定匹配次數
指定方式 | 含義 |
---|---|
{n} | 匹配之前的元素n次 |
{n,m} | 匹配之前的元素至少n次,至多m次 |
{n,} | 匹配之前的元素至少n次,最多不限 |
{,m} | 匹配之前的元素不超過m次 |
實用{}將前文的電話號碼例子,簡化為
‘^/( ?[0-9]{3} /)? [0-9] {3} - [0-9] {4}$’
匹配符合(nnn)nnn-nnnn的電話號碼:
echo "(555) 123-4576" | grep -E '^/(?[0-9]{3}/)?[0-9]{3}-[0-9]{4}$'
輸出結果如下:
(555) 123-4576
結果表明可以匹配此類格式的電話號碼,與前一種寫法結果相同,但是書寫更方便。
匹配符合nnn nnn-nnnn的電話號碼:
echo "555 123-4576" | grep -E '^/(?[0-9]{3}/)?[0-9]{3}-[0-9]{4}$'
輸出結果如下:
555 123-4576
結果表明可以匹配此類格式的電話號碼,與前一種寫法結果相同,但是書寫更方便。
匹配不符合的情況:
echo "AAA 123-4567" | grep -E '^/(?[0-9]{3}/)?[0-9]{3}-[0-9]{4}$'
沒有輸出結果,說明與預期相符合,可以替代原來的書寫方式。
find使用正則表達式
find path -regex 'stirng' #在path目錄查找匹配正則表達式string的文件
locate使用正則表達式
locate即支持BRE(–regexp選項)也支持ERE(–regex選項)。
locate --regex 'string' #在/目錄查找匹配正則表達式string的文件
Less和Vim搜索文本
Less下的正則表達式:
/‘regex’
Less會高亮出匹配項,這樣就很容易分辨出無效號碼:
Vim只支持BRE,因此用于搜索的正則表達式得改寫成這樣
/([0-9]{3}) [0-9]{3}-[0-9]{4}
對于ERE支持的符號需要在其前面加上\。