接下來的幾章中,我們將會看一下一些用來操作文本的工具。正如我們所見到的,在類 Unix 的 操作系統(tǒng)中,比如 Linux 中,文本數(shù)據(jù)起著舉足輕重的作用。但是在我們能完全理解這些工具提供的 所有功能之前,我們不得不先看看,經(jīng)常與這些工具的高級使用相關聯(lián)的一門技術——正則表達式。
我們已經(jīng)瀏覽了許多由命令行提供的功能和工具,我們遇到了一些真正神秘的 shell 功能和命令, 比如 shell 展開和引用,鍵盤快捷鍵,和命令歷史,更不用說 vi 編輯器了。正則表達式延續(xù)了 這種“傳統(tǒng)”,而且有可能(備受爭議地)是其中最神秘的功能。這并不是說花費時間來學習它們 是不值得的,而是恰恰相反。雖然它們的全部價值可能不能立即顯現(xiàn),但是較強理解這些功能 使我們能夠表演令人驚奇的技藝。什么是正則表達式?
簡而言之,正則表達式是一種符號表示法,被用來識別文本模式。在某種程度上,它們與匹配 文件和路徑名的 shell 通配符比較相似,但其規(guī)模更龐大。許多命令行工具和大多數(shù)的編程語言 都支持正則表達式,以此來幫助解決文本操作問題。然而,并不是所有的正則表達式都是一樣的, 這就進一步混淆了事情;不同工具以及不同語言之間的正則表達式都略有差異。我們將會限定 POSIX 標準中描述的正則表達式(其包括了大多數(shù)的命令行工具),供我們討論, 與許多編程語言(最著名的 Perl 語言)相反,它們使用了更多和更豐富的符號集。
我們將使用的主要程序是我們的老朋友,grep 程序,它會用到正則表達式。實際上,“grep”這個名字 來自于短語“global regular expression print”,所以我們能看出 grep 程序和正則表達式有關聯(lián)。 本質(zhì)上,grep 程序會在文本文件中查找一個指定的正則表達式,并把匹配行輸出到標準輸出。
到目前為止,我們已經(jīng)使用 grep 程序查找了固定的字符串,就像這樣:
[me@linuxbox ~]$ ls /usr/bin | grep zip
這個命令會列出,位于目錄 /usr/bin 中,文件名中包含子字符串“zip”的所有文件。
這個 grep 程序以這樣的方式來接受選項和參數(shù):
grep [options] regex [file...]
這里的 regx 是指一個正則表達式。
這是一個常用的 grep 選項列表:
選項 | 描述 |
---|---|
-i | 忽略大小寫。不會區(qū)分大小寫字符。也可用--ignore-case 來指定。 |
-v | 不匹配。通常,grep 程序會打印包含匹配項的文本行。這個選項導致 grep 程序 只會不包含匹配項的文本行。也可用--invert-match 來指定。 |
-c | 打印匹配的數(shù)量(或者是不匹配的數(shù)目,若指定了-v 選項),而不是文本行本身。 也可用--count 選項來指定。 |
-l | 打印包含匹配項的文件名,而不是文本行本身,也可用--files-with-matches 選項來指定。 |
-L | 相似于-l 選項,但是只是打印不包含匹配項的文件名。也可用--files-without-match 來指定。 |
-n | 在每個匹配行之前打印出其位于文件中的相應行號。也可用--line-number 選項來指定。 |
-h | 應用于多文件搜索,不輸出文件名。也可用--no-filename 選項來指定。 |
為了更好的探究 grep 程序,讓我們創(chuàng)建一些文本文件來搜尋:
[me@linuxbox ~]$ ls /bin > dirlist-bin.txt
[me@linuxbox ~]$ ls /usr/bin > dirlist-usr-bin.txt
[me@linuxbox ~]$ ls /sbin > dirlist-sbin.txt
[me@linuxbox ~]$ ls /usr/sbin > dirlist-usr-sbin.txt
[me@linuxbox ~]$ ls dirlist*.txt
dirlist-bin.txt dirlist-sbin.txt dirlist-usr-sbin.txt
dirlist-usr-bin.txt
我們能夠?qū)ξ覀兊奈募斜韴?zhí)行簡單的搜索,像這樣:
[me@linuxbox ~]$ grep bzip dirlist*.txt
dirlist-bin.txt:bzip2
dirlist-bin.txt:bzip2recover
在這個例子里,grep 程序在所有列出的文件中搜索字符串 bzip,然后找到兩個匹配項,其都在 文件 dirlist-bin.txt 中。如果我們只是對包含匹配項的文件列表,而不是對匹配項本身感興趣 的話,我們可以指定-l 選項:
[me@linuxbox ~]$ grep -l bzip dirlist*.txt
dirlist-bin.txt
相反地,如果我們只想查看不包含匹配項的文件列表,我們可以這樣操作:
[me@linuxbox ~]$ grep -L bzip dirlist*.txt
dirlist-sbin.txt
dirlist-usr-bin.txt
dirlist-usr-sbin.txt
它可能看起來不明顯,但是我們的 grep 程序一直使用了正則表達式,雖然是非常簡單的例子。 這個正則表達式“bzip”意味著,匹配項所在行至少包含4個字符,并且按照字符 “b”, “z”, “i”, 和 “p”的順序 出現(xiàn)在匹配行的某處,字符之間沒有其它的字符。字符串“bzip”中的所有字符都是原義字符,因為 它們匹配本身。除了原義字符之外,正則表達式也可能包含元字符,其被用來指定更復雜的匹配項。 正則表達式元字符由以下字符組成:
^ $ . [ ] { } - ? * + ( ) | \
然后其它所有字符都被認為是原義字符,雖然在個別情況下,反斜杠會被用來創(chuàng)建元序列, 也允許元字符被轉(zhuǎn)義為原義字符,而不是被解釋為元字符。
注意:正如我們所見到的,當 shell 執(zhí)行展開的時候,許多正則表達式元字符,也是對 shell 有特殊 含義的字符。當我們在命令行中傳遞包含元字符的正則表達式的時候,把元字符用引號引起來至關重要, 這樣可以阻止 shell 試圖展開它們。
我們將要查看的第一個元字符是圓點字符,其被用來匹配任意字符。如果我們在正則表達式中包含它, 它將會匹配在此位置的任意一個字符。這里有個例子:
[me@linuxbox ~]$ grep -h '.zip' dirlist*.txt
bunzip2
bzip2
bzip2recover
gunzip
gzip
funzip
gpg-zip
preunzip
prezip
prezip-bin
unzip
unzipsfx
我們在文件中查找包含正則表達式“.zip”的文本行。對于搜索結(jié)果,有幾點需要注意一下。 注意沒有找到這個 zip 程序。這是因為在我們的正則表達式中包含的圓點字符把所要求的匹配項的長度 增加到四個字符,并且字符串“zip”只包含三個字符,所以這個 zip 程序不匹配。另外,如果我們的文件列表 中有一些文件的擴展名是.zip,則它們也會成為匹配項,因為文件擴展名中的圓點符號也會被看作是 “任意字符”。
在正則表達式中,插入符號和美元符號被看作是錨點。這意味著正則表達式 只有在文本行的開頭或末尾被找到時,才算發(fā)生一次匹配。
[me@linuxbox ~]$ grep -h '^zip' dirlist*.txt
zip
zipcloak
zipgrep
zipinfo
zipnote
zipsplit
[me@linuxbox ~]$ grep -h 'zip$' dirlist*.txt
gunzip
gzip
funzip
gpg-zip
preunzip
prezip
unzip
zip
[me@linuxbox ~]$ grep -h '^zip$' dirlist*.txt
zip
這里我們分別在文件列表中搜索行首,行尾以及行首和行尾同時包含字符串“zip”(例如,zip 獨占一行)的匹配行。 注意正則表達式‘^$’(行首和行尾之間沒有字符)會匹配空行。
字謎助手
到目前為止,甚至憑借我們有限的正則表達式知識,我們已經(jīng)能做些有意義的事情了。
我妻子喜歡玩字謎游戲,有時候她會因為一個特殊的問題,而向我求助。類似這樣的問題,“一個 有五個字母的單詞,它的第三個字母是‘j’,最后一個字母是‘r’,是哪個單詞?”這類問題會 讓我動腦筋想想。
你知道你的 Linux 系統(tǒng)中帶有一本英文字典嗎?千真萬確。看一下 /usr/share/dict 目錄,你就能找到一本, 或幾本。存儲在此目錄下的字典文件,其內(nèi)容僅僅是一個長長的單詞列表,每行一個單詞,按照字母順序排列。在我的 系統(tǒng)中,這個文件僅包含98,000個單詞。為了找到可能的上述字謎的答案,我們可以這樣做:
[me@linuxbox ~]$ grep -i '^..j.r$' /usr/share/dict/words Major major
使用這個正則表達式,我們能在我們的字典文件中查找到包含五個字母,且第三個字母 是“j”,最后一個字母是“r”的所有單詞。
除了能夠在正則表達式中的給定位置匹配任意字符之外,通過使用中括號表達式, 我們也能夠從一個指定的字符集合中匹配一個單個的字符。通過中括號表達式,我們能夠指定 一個字符集合(包含在不加中括號的情況下會被解釋為元字符的字符)來被匹配。在這個例子里,使用了一個兩個字符的集合:
[me@linuxbox ~]$ grep -h '[bg]zip' dirlist*.txt
bzip2
bzip2recover
gzip
我們匹配包含字符串“bzip”或者“gzip”的任意行。
一個字符集合可能包含任意多個字符,并且元字符被放置到中括號里面后會失去了它們的特殊含義。 然而,在兩種情況下,會在中括號表達式中使用元字符,并且有著不同的含義。第一個元字符 是插入字符,其被用來表示否定;第二個是連字符字符,其被用來表示一個字符區(qū)域。
如果在正則表示式中的第一個字符是一個插入字符,則剩余的字符被看作是不會在給定的字符位置出現(xiàn)的 字符集合。通過修改之前的例子,我們試驗一下:
[me@linuxbox ~]$ grep -h '[^bg]zip' dirlist*.txt
bunzip2
gunzip
funzip
gpg-zip
preunzip
prezip
prezip-bin
unzip
unzipsfx
通過激活否定操作,我們得到一個文件列表,它們的文件名都包含字符串“zip”,并且“zip”的前一個字符 是除了“b”和“g”之外的任意字符。注意文件 zip 沒有被發(fā)現(xiàn)。一個否定的字符集仍然在給定位置要求一個字符, 但是這個字符必須不是否定字符集的成員。
這個插入字符如果是中括號表達式中的第一個字符的時候,才會喚醒否定功能;否則,它會失去 它的特殊含義,變成字符集中的一個普通字符。
如果我們想要構(gòu)建一個正則表達式,它可以在我們的列表中找到每個以大寫字母開頭的文件,我們 可以這樣做:
[me@linuxbox ~]$ grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXZY]' dirlist*.txt
這只是一個在正則表達式中輸入26個大寫字母的問題。但是輸入所有字母非常令人煩惱,所以有另外一種方式:
[me@linuxbox ~]$ grep -h '^[A-Z]' dirlist*.txt
MAKEDEV
ControlPanel
GET
HEAD
POST
X
X11
Xorg
MAKEFLOPPIES
NetworkManager
NetworkManagerDispatcher
通過使用一個三字符區(qū)域,我們能夠縮寫26個字母。任意字符的區(qū)域都能按照這種方式表達,包括多個區(qū)域, 比如下面這個表達式就匹配了所有以字母和數(shù)字開頭的文件名:
[me@linuxbox ~]$ grep -h '^[A-Za-z0-9]' dirlist*.txt
在字符區(qū)域中,我們看到這個連字符被特殊對待,所以我們怎樣在一個正則表達式中包含一個連字符呢? 方法就是使連字符成為表達式中的第一個字符??紤]一下這兩個例子:
[me@linuxbox ~]$ grep -h '[A-Z]' dirlist*.txt
這會匹配包含一個大寫字母的文件名。然而:
[me@linuxbox ~]$ grep -h '[-AZ]' dirlist*.txt
上面的表達式會匹配包含一個連字符,或一個大寫字母“A”,或一個大寫字母“Z”的文件名。
這個傳統(tǒng)的字符區(qū)域在處理快速地指定字符集合的問題方面,是一個易于理解的和有效的方式。 不幸地是,它們不總是工作。到目前為止,雖然我們在使用 grep 程序的時候沒有遇到任何問題, 但是我們可能在使用其它程序的時候會遭遇困難。
回到第5章,我們看看通配符怎樣被用來完成路徑名展開操作。在那次討論中,我們說過在 某種程度上,那個字符區(qū)域被使用的方式幾乎與在正則表達式中的用法一樣,但是有一個問題:
[me@linuxbox ~]$ ls /usr/sbin/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*
/usr/sbin/MAKEFLOPPIES
/usr/sbin/NetworkManagerDispatcher
/usr/sbin/NetworkManager
(依賴于不同的 Linux 發(fā)行版,我們將得到不同的文件列表,有可能是一個空列表。這個例子來自于 Ubuntu) 這個命令產(chǎn)生了期望的結(jié)果——只有以大寫字母開頭的文件名,但是:
[me@linuxbox ~]$ 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
通過這個命令我們得到整個不同的結(jié)果(只顯示了一部分結(jié)果列表)。為什么會是那樣? 說來話長,但是這個版本比較簡短:
追溯到 Unix 剛剛開發(fā)的時候,它只知道 ASCII 字符,并且這個特性反映了事實。在 ASCII 中,前32個字符 (數(shù)字0-31)都是控制碼(如 tabs,backspaces,和回車)。隨后的32個字符(32-63)包含可打印的字符, 包括大多數(shù)的標點符號和數(shù)字0到9。再隨后的32個字符(64-95)包含大寫字符和一些更多的標點符號。 最后的31個字符(96-127)包含小寫字母和更多的標點符號?;谶@種安排方式,系統(tǒng)使用這種排序規(guī)則 的 ASCII:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
這個不同于正常的字典順序,其像這樣:
aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ
隨著 Unix 系統(tǒng)的知名度在美國之外的國家傳播開來,就需要支持不在 U.S.英語范圍內(nèi)的字符。 于是就擴展了這個 ASCII 字符表,使用了整個8位,添加了字符(數(shù)字128-255),這樣就 容納了更多的語言。
為了支持這種能力,POSIX 標準介紹了一種叫做 locale 的概念,其可以被調(diào)整,來為某個特殊的區(qū)域, 選擇所需的字符集。通過使用下面這個命令,我們能夠查看到我們系統(tǒng)的語言設置:
[me@linuxbox ~]$ echo $LANG
en_US.UTF-8
通過這個設置,POSIX 相容的應用程序?qū)褂米值渑帕许樞蚨皇?ASCII 順序。這就解釋了上述命令的行為。 當[A-Z]字符區(qū)域按照字典順序解釋的時候,包含除了小寫字母“a”之外的所有字母,因此得到這樣的結(jié)果。
為了部分地解決這個問題,POSIX 標準包含了大量的字符集,其提供了有用的字符區(qū)域。 下表中描述了它們:
字符集 | 說明 |
---|---|
[:alnum:] | 字母數(shù)字字符。在 ASCII 中,等價于:[A-Za-z0-9] |
[:word:] | 與[:alnum:]相同, 但增加了下劃線字符。 |
[:alpha:] | 字母字符。在 ASCII 中,等價于:[A-Za-z] |
[:blank:] | 包含空格和 tab 字符。 |
[:cntrl:] | ASCII 的控制碼。包含了0到31,和127的 ASCII 字符。 |
[:digit:] | 數(shù)字0到9 |
[:graph:] | 可視字符。在 ASCII 中,它包含33到126的字符。 |
[:lower:] | 小寫字母。 |
[:punct:] | 標點符號字符。在 ASCII 中,等價于: |
[:print:] | 可打印的字符。在[:graph:]中的所有字符,再加上空格字符。 |
[:space:] | 空白字符,包括空格,tab,回車,換行,vertical tab, 和 form feed.在 ASCII 中, 等價于:[ \t\r\n\v\f] |
[:upper:] | 大寫字母。 |
[:xdigit:] | 用來表示十六進制數(shù)字的字符。在 ASCII 中,等價于:[0-9A-Fa-f] |
甚至通過字符集,仍然沒有便捷的方法來表達部分區(qū)域,比如[A-M]。
通過使用字符集,我們重做上述的例題,看到一個改進的結(jié)果:
[me@linuxbox ~]$ ls /usr/sbin/[[:upper:]]*
/usr/sbin/MAKEFLOPPIES
/usr/sbin/NetworkManagerDispatcher
/usr/sbin/NetworkManager
記住,然而,這不是一個正則表達式的例子,而是 shell 正在執(zhí)行路徑名展開操作。我們在這里展示這個例子, 是因為 POSIX 規(guī)范的字符集適用于二者。
恢復到傳統(tǒng)的排列順序
通過改變環(huán)境變量 LANG 的值,你可以選擇讓你的系統(tǒng)使用傳統(tǒng)的(ASCII)排列規(guī)則。如上所示,這個 LANG 變量包含了語種和字符集。這個值最初由你安裝 Linux 系統(tǒng)時所選擇的安裝語言決定。
使用 locale 命令,來查看 locale 的設置。
[me@linuxbox ~]$ locale LANG=en_US.UTF-8 LC_CTYPE="en_US.UTF-8" LC_NUMERIC="en_US.UTF-8" LC_TIME="en_US.UTF-8" LC_COLLATE="en_US.UTF-8" LC_MONETARY="en_US.UTF-8" LC_MESSAGES="en_US.UTF-8" LC_PAPER="en_US.UTF-8" LC_NAME="en_US.UTF-8" LC_ADDRESS="en_US.UTF-8" LC_TELEPHONE="en_US.UTF-8" LC_MEASUREMENT="en_US.UTF-8" LC_IDENTIFICATION="en_US.UTF-8" LC_ALL=
把這個 LANG 變量設置為 POSIX,來更改 locale,使其使用傳統(tǒng)的 Unix 行為。
[me@linuxbox ~]$ export LANG=POSIX
注意這個改動使系統(tǒng)為它的字符集使用 U.S.英語(更準確地說,ASCII),所以要確認一下這 是否是你真正想要的效果。通過把這條語句添加到你的.bashrc 文件中,你可以使這個更改永久有效。
export LANG=POSIX
就在我們認為這已經(jīng)非常令人困惑了,我們卻發(fā)現(xiàn) POSIX 把正則表達式的實現(xiàn)分成了兩類: 基本正則表達式(BRE)和擴展的正則表達式(ERE)。既服從 POSIX 規(guī)范又實現(xiàn)了 BRE 的任意應用程序,都支持我們目前研究的所有正則表達式特性。我們的 grep 程序就是其中一個。
BRE 和 ERE 之間有什么區(qū)別呢?這是關于元字符的問題。BRE 可以辨別以下元字符:
^ $ . [ ] *
其它的所有字符被認為是文本字符。ERE 添加了以下元字符(以及與其相關的功能):
( ) { } ? + |
然而(這也是有趣的地方),在 BRE 中,字符“(”,“)”,“{”,和 “}”用反斜杠轉(zhuǎn)義后,被看作是元字符, 相反在 ERE 中,在任意元字符之前加上反斜杠會導致其被看作是一個文本字符。在隨后的討論中將會涵蓋 很多奇異的特性。
因為我們將要討論的下一個特性是 ERE 的一部分,我們將要使用一個不同的 grep 程序。照慣例, 一直由 egrep 程序來執(zhí)行這項操作,但是 GUN 版本的 grep 程序也支持擴展的正則表達式,當使用了-E 選項之后。
在 20 世紀 80 年代,Unix 成為一款非常流行的商業(yè)操作系統(tǒng),但是到了1988年,Unix 世界 一片混亂。許多計算機制造商從 Unix 的創(chuàng)建者 AT&T 那里得到了許可的 Unix 源碼,并且 供應各種版本的操作系統(tǒng)。然而,在他們努力創(chuàng)造產(chǎn)品差異化的同時,每個制造商都增加了 專用的更改和擴展。這就開始限制了軟件的兼容性。
專有軟件供應商一如既往,每個供應商都試圖玩嬴游戲“鎖定”他們的客戶。這個 Unix 歷史上 的黑暗時代,就是今天眾所周知的 “the Balkanization”。
然后進入 IEEE( 電氣與電子工程師協(xié)會 )時代。在上世紀 80 年代中葉,IEEE 開始制定一套標準, 其將會定義 Unix 系統(tǒng)( 以及類 Unix 的系統(tǒng) )如何執(zhí)行。這些標準,正式成為 IEEE 1003, 定義了應用程序編程接口( APIs ),shell 和一些實用程序,其將會在標準的類 Unix 操作系統(tǒng)中找到?!癙OSIX” 這個名字,象征著可移植的操作系統(tǒng)接口(為了額外的,添加末尾的 “X” ), 是由 Richard Stallman 建議的( 是的,的確是 Richard Stallman ),后來被 IEEE 采納。
我們將要討論的擴展表達式的第一個特性叫做 alternation(交替),其是一款允許從一系列表達式 之間選擇匹配項的實用程序。就像中括號表達式允許從一系列指定的字符之間匹配單個字符那樣, alternation 允許從一系列字符串或者是其它的正則表達式中選擇匹配項。為了說明問題, 我們將會結(jié)合 echo 程序來使用 grep 命令。首先,讓我們試一個普通的字符串匹配:
[me@linuxbox ~]$ echo "AAA" | grep AAA
AAA
[me@linuxbox ~]$ echo "BBB" | grep AAA
[me@linuxbox ~]$
一個相當直截了當?shù)睦?,我們?echo 的輸出管道給 grep,然后看到輸出結(jié)果。當出現(xiàn) 一個匹配項時,我們看到它會打印出來;當沒有匹配項時,我們看到?jīng)]有輸出結(jié)果。
現(xiàn)在我們將添加 alternation,以豎杠線元字符為標記:
[me@linuxbox ~]$ echo "AAA" | grep -E 'AAA|BBB'
AAA
[me@linuxbox ~]$ echo "BBB" | grep -E 'AAA|BBB'
BBB
[me@linuxbox ~]$ echo "CCC" | grep -E 'AAA|BBB'
[me@linuxbox ~]$
這里我們看到正則表達式'AAA|BBB',這意味著“匹配字符串 AAA 或者是字符串 BBB”。注意因為這是 一個擴展的特性,我們給 grep 命令(雖然我們能以 egrep 程序來代替)添加了-E 選項,并且我們 把這個正則表達式用單引號引起來,為的是阻止 shell 把豎杠線元字符解釋為一個 pipe 操作符。 Alternation 并不局限于兩種選擇:
[me@linuxbox ~]$ echo "AAA" | grep -E 'AAA|BBB|CCC'
AAA
為了把 alternation 和其它正則表達式元素結(jié)合起來,我們可以使用()來分離 alternation。
[me@linuxbox ~]$ grep -Eh '^(bz|gz|zip)' dirlist*.txt
這個表達式將會在我們的列表中匹配以“bz”,或“gz”,或“zip”開頭的文件名。如果我們刪除了圓括號, 這個表達式的意思:
[me@linuxbox ~]$ grep -Eh '^bz|gz|zip' dirlist*.txt
會變成匹配任意以“bz”開頭,或包含“gz”,或包含“zip”的文件名。
擴展的正則表達式支持幾種方法,來指定一個元素被匹配的次數(shù)。
這個限定符意味著,實際上,“使前面的元素可有可無?!北确秸f我們想要查看一個電話號碼的真實性, 如果它匹配下面兩種格式的任意一種,我們就認為這個電話號碼是真實的:
(nnn) nnn-nnnn
nnn nnn-nnnn
這里的“n”是一個數(shù)字。我們可以構(gòu)建一個像這樣的正則表達式:
^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$
在這個表達式中,我們在圓括號之后加上一個問號,來表示它們將被匹配零次或一次。再一次,因為 通常圓括號都是元字符(在 ERE 中),所以我們在圓括號之前加上了反斜杠,使它們成為文本字符。
讓我們試一下:
[me@linuxbox ~]$ echo "(555) 123-4567" | grep -E '^\(?[0-9][0-9][0-9]
\)? [0-9][0-9][0-9]$'
(555) 123-4567
[me@linuxbox ~]$ 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
[me@linuxbox ~]$ 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]$'
[me@linuxbox ~]$
這里我們看到這個表達式匹配這個電話號碼的兩種形式,但是不匹配包含非數(shù)字字符的號碼。
像 ? 元字符一樣,這個 * 被用來表示一個可選的字符;然而,又與 ? 不同,匹配的字符可以出現(xiàn) 任意多次,不僅是一次。比方說我們想要知道是否一個字符串是一句話;也就是說,字符串開始于 一個大寫字母,然后包含任意多個大寫和小寫的字母和空格,最后以句號收尾。為了匹配這個(非常粗略的) 語句的定義,我們能夠使用一個像這樣的正則表達式:
[[:upper:]][[:upper:][:lower:] ]*.
這個表達式由三個元素組成:一個包含[:upper:]字符集的中括號表達式,一個包含[:upper:]和[:lower:] 兩個字符集以及一個空格的中括號表達式,和一個被反斜杠字符轉(zhuǎn)義過的圓點。第二個元素末尾帶有一個 *元字符,所以在開頭的大寫字母之后,可能會跟隨著任意數(shù)目的大寫和小寫字母和空格,并且匹配:
[me@linuxbox ~]$ echo "This works." | grep -E '[[:upper:]][[:upper:][[:lower:]]*.'
This works.
[me@linuxbox ~]$ echo "This Works." | grep -E '[[:upper:]][[:upper:][[:lower:]]*.'
This Works.
[me@linuxbox ~]$ echo "this does not" | grep -E '[[:upper:]][[:upper: ][[:lower:]]*.'
[me@linuxbox ~]$
這個表達式匹配前兩個測試語句,但不匹配第三個,因為第三個句子缺少開頭的大寫字母和末尾的句號。
這個 + 元字符的作用與 * 非常相似,除了它要求前面的元素至少出現(xiàn)一次匹配。這個正則表達式只匹配 那些由一個或多個字母字符組構(gòu)成的文本行,字母字符之間由單個空格分開:
^([[:alpha:]]+ ?)+$
[me@linuxbox ~]$ echo "This that" | grep -E '^([[:alpha:]]+ ?)+$'
This that
[me@linuxbox ~]$ echo "a b c" | grep -E '^([[:alpha:]]+ ?)+$'
a b c
[me@linuxbox ~]$ echo "a b 9" | grep -E '^([[:alpha:]]+ ?)+$'
[me@linuxbox ~]$ echo "abc d" | grep -E '^([[:alpha:]]+ ?)+$'
[me@linuxbox ~]$
我們看到這個正則表達式不匹配“a b 9”這一行,因為它包含了一個非字母的字符;它也不匹配 “abc d” ,因為在字符“c”和“d”之間不止一個空格。
這個 { 和 } 元字符都被用來表達要求匹配的最小和最大數(shù)目。它們可以通過四種方法來指定:
限定符 | 意思 |
---|---|
{n} | 匹配前面的元素,如果它確切地出現(xiàn)了 n 次。 |
{n,m} | 匹配前面的元素,如果它至少出現(xiàn)了 n 次,但是不多于 m 次。 |
{n,} | 匹配前面的元素,如果它出現(xiàn)了 n 次或多于 n 次。 |
{,m} | 匹配前面的元素,如果它出現(xiàn)的次數(shù)不多于 m 次。 |
回到之前處理電話號碼的例子,我們能夠使用這種指定重復次數(shù)的方法來簡化我們最初的正則表達式:
^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$
簡化為:
^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$
讓我們試一下:
[me@linuxbox ~]$ echo "(555) 123-4567" | grep -E '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$'
(555) 123-4567
[me@linuxbox ~]$ echo "555 123-4567" | grep -E '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$'
555 123-4567
[me@linuxbox ~]$ echo "5555 123-4567" | grep -E '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$'
[me@linuxbox ~]$
我們可以看到,我們修訂的表達式能成功地驗證帶有和不帶有圓括號的數(shù)字,而拒絕那些格式 不正確的數(shù)字。
讓我們看看一些我們已經(jīng)知道的命令,然后看一下它們怎樣使用正則表達式。
在我們先前的例子中,我們查看過單個電話號碼,并且檢查了它們的格式。一個更現(xiàn)實的 情形是檢查一個數(shù)字列表,所以我們先創(chuàng)建一個列表。我們將背誦一個神奇的咒語到命令行中。 它會很神奇,因為我們還沒有涵蓋所涉及的大部分命令,但是不要擔心。我們將在后面的章節(jié)里面 討論那些命令。這里是這個咒語:
[me@linuxbox ~]$ for i in {1..10}; do echo "(${RANDOM:0:3}) ${RANDO
M:0:3}-${RANDOM:0:4}" >> phonelist.txt; done
這個命令會創(chuàng)建一個包含10個電話號碼的名為 phonelist.txt 的文件。每次重復這個命令的時候, 另外10個號碼會被添加到這個列表中。我們也能夠更改命令開頭附近的數(shù)值10,來生成或多或少的 電話號碼。如果我們查看這個文件的內(nèi)容,然而我們會發(fā)現(xiàn)一個問題:
[me@linuxbox ~]$ cat phonelist.txt
(232) 298-2265
(624) 381-1078
(540) 126-1980
(874) 163-2885
(286) 254-2860
(292) 108-518
(129) 44-1379
(458) 273-1642
(686) 299-8268
(198) 307-2440
一些號碼是殘缺不全的,但是它們很適合我們的需求,因為我們將使用 grep 命令來驗證它們。
一個有用的驗證方法是掃描這個文件,查找無效的號碼,并把搜索結(jié)果顯示到屏幕上:
[me@linuxbox ~]$ grep -Ev '^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$'
phonelist.txt
(292) 108-518
(129) 44-1379
[me@linuxbox ~]$
這里我們使用-v 選項來產(chǎn)生相反的匹配,因此我們將只輸出不匹配指定表達式的文本行。這個 表達式自身的兩端都包含定位點(錨)元字符,是為了確保這個號碼的兩端沒有多余的字符。 這個表達式也要求圓括號出現(xiàn)在一個有效的號碼中,不同于我們先前電話號碼的實例。
這個 find 命令支持一個基于正則表達式的測試。當在使用正則表達式方面比較 find 和 grep 命令的時候, 還有一個重要問題要牢記在心。當某一行包含的字符串匹配上了一個表達式的時候,grep 命令會打印出這一行, 然而 find 命令要求路徑名精確地匹配這個正則表達式。在下面的例子里面,我們將使用帶有一個正則 表達式的 find 命令,來查找每個路徑名,其包含的任意字符都不是以下字符集中的一員。
[-\_./0-9a-zA-Z]
這樣一種掃描會發(fā)現(xiàn)包含空格和其它潛在不規(guī)范字符的路徑名:
[me@linuxbox ~]$ find . -regex '.*[^-\_./0-9a-zA-Z].*'
由于要精確地匹配整個路徑名,所以我們在表達式的兩端使用了.*,來匹配零個或多個字符。 在表達式中間,我們使用了否定的中括號表達式,其包含了我們一系列可接受的路徑名字符。
這個 locate 程序支持基本的(--regexp 選項)和擴展的(--regex 選項)正則表達式。通過 locate 命令,我們能夠執(zhí)行許多與先前操作 dirlist 文件時相同的操作:
[me@linuxbox ~]$ locate --regex 'bin/(bz|gz|zip)'
/bin/bzcat
/bin/bzcmp
/bin/bzdiff
/bin/bzegrep
/bin/bzexe
/bin/bzfgrep
/bin/bzgrep
/bin/bzip2
/bin/bzip2recover
/bin/bzless
/bin/bzmore
/bin/gzexe
/bin/gzip
/usr/bin/zip
/usr/bin/zipcloak
/usr/bin/zipgrep
/usr/bin/zipinfo
/usr/bin/zipnote
/usr/bin/zipsplit
通過使用 alternation,我們搜索包含 bin/bz,bin/gz,或/bin/zip 字符串的路徑名。
less 和 vim 兩者享有相同的文本查找方法。按下/按鍵,然后輸入正則表達式,來執(zhí)行搜索任務。 如果我們使用 less 程序來瀏覽我們的 phonelist.txt 文件:
[me@linuxbox ~]$ less phonelist.txt
然后查找我們有效的表達式:
(232) 298-2265
(624) 381-1078
(540) 126-1980
(874) 163-2885
(286) 254-2860
(292) 108-518
(129) 44-1379
(458) 273-1642
(686) 299-8268
(198) 307-2440
~
/^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$
less 將會高亮匹配到的字符串,這樣就很容易看到無效的電話號碼:
(232) 298-2265
(624) 381-1078
(540) 126-1980
(874) 163-2885
(286) 254-2860
(292) 108-518
(129) 44-1379
(458) 273-1642
(686) 299-8268
(198) 307-2440
~
(END)
另一方面,vim 支持基本的正則表達式,所以我們用于搜索的表達式看起來像這樣:
/([0-9]\{3\}) [0-9]\{3\}-[0-9]\{4\}
我們看到表達式幾乎一樣;然而,在擴展表達式中,許多被認為是元字符的字符在基本的表達式 中被看作是文本字符。只有用反斜杠把它們轉(zhuǎn)義之后,它們才被看作是元字符。
依賴于系統(tǒng)中 vim 的特殊配置,匹配項將會被高亮。如若不是,試試這個命令模式:
:hlsearch
來激活搜索高亮功能。
注意:依賴于你的發(fā)行版,vim 有可能支持或不支持文本搜索高亮功能。尤其是 Ubuntu 自帶了 一款非常簡化的 vim 版本。在這樣的系統(tǒng)中,你可能要使用你的軟件包管理器來安裝一個功能 更完備的 vim 版本。
在這章中,我們已經(jīng)看到幾個使用正則表達式例子。如果我們使用正則表達式來搜索那些使用正則表達式的應用程序, 我們可以找到更多的使用實例。通過查找手冊頁,我們就能找到:
[me@linuxbox ~]$ cd /usr/share/man/man1
[me@linuxbox man1]$ zgrep -El 'regex|regular expression' *.gz
這個 zgrep 程序是 grep 的前端,允許 grep 來讀取壓縮文件。在我們的例子中,我們在手冊文件所在的 目錄中,搜索壓縮文件中的內(nèi)容。這個命令的結(jié)果是一個包含字符串“regex”或者“regular expression”的文件列表。正如我們所看到的,正則表達式會出現(xiàn)在大量程序中。
基本正則表達式中有一個特性,我們沒有涵蓋。叫做反引用,這個特性在下一章中會被討論到。
有許多在線學習正則表達式的資源,包括各種各樣的教材和速記表。
另外,關于下面的背景話題,Wikipedia 有不錯的文章。