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