Linux基础:正则表格式

Linux基础12:正则表格式
2005-01-22    admin       点击: 2304
Linux基础12:正则表格式
正規表示法 Regular Expression, RE(積極施工)
最近更新日期:2005/01/28

正規表示法(或稱為常規表示法)是透過一些特殊字元的排列,用以 搜尋/取代/刪除 一列或多列文字字串, 簡單的說,正規表示法就是用在字串的處理上面的一項『表示式』。正規表示法並不是一個工具程式, 而是一個字串處理的標準依據,如果您想要以正規表示法的方式處理字串,就得要使用支援正規表示法的工具程式才行, 這類的工具程式很多,例如 vi, sed, awk 等等。

正規表示法對於系統管理員來說,實在是很重要。因為系統會產生很多的訊息,這些訊息有的重要,有的僅是告知, 此時,管理員可以特過正規表示法的功能來將重要訊息擷取出來,並產生便於查閱的報表,簡化管理流程。此外, 很多的套裝軟體也都支援正規表示法的分析,例如郵件伺服器的過濾機制(過濾垃圾信件)就是很重要的一個例子。 所以,您最好要瞭解正規表示法的相關技能,在未來管理主機時,才能夠更精簡處理您的日常事務!

註:本章節使用者需要多加練習,因為目前很多的套件都是使用正規表示法來達成其『過濾、分析』的目的, 為了未來主機管理的便利性,使用者至少要能看的懂正規表示法的意義!

前言:
基礎正規表示法:
  :以 grep 擷取字串
  :重要特殊字元(characters)
延伸正規表示法:
sed 工具簡介
awk 工具簡介
常用的相關字元指令:

  :tr
  :col
  :expand, unexpand
  :paste,
  :xargs

好用的搜尋軟體 sed 簡介
關於中文字串的處理問題
厲害的 awk 簡介
正規表示法與管線命令之應用
相關應用:
  :檔案的比對 diff, cmp, ....
重點回顧

本章與 LPI 的關係

參考資源

本章習題練習

大標題的圖示前言

約略瞭解了 Linux 的基本指令 ( Shell ) 並且熟悉了 vi 之後,相信您對於敲擊鍵盤與指令比較不陌生了吧?? 接下來,底下要開始介紹一個很重要的觀念,那就是所謂的『 正規表示法』囉!


小標題的圖示 什麼是正規表示法
任何一個有經驗的系統管理員,都會告訴您:『 正規表示法真是挺重要的!』 為什麼很重要呢?因為日常生活就使用的到啊!舉個例子來說, 在您日常處理文書作業時,應該會常常使用到『搜尋/取代』等等的功能吧? 這些舉動要作的漂亮,就是正規表示法的工作了!

簡單的說,正規表示法就是處理字串的方法,他是以行為單位, 來進行字串的處理行為,他透過一些特殊符號的輔助,可以讓使用者輕易的達到 搜尋/取代 某特定字串的處理程序!

舉例來說,我要找到 VBird 或 Vbird 這個字樣,但是不要其他的字串,該如何辦理? 如果在沒有正規表示法的環境中(例如 MS word),您或許就得要使用忽略大小寫的辦法, 或者是分別以 VBird 及 Vbird 搜尋兩遍。但是,忽略大小寫可能會搜尋到 VBIRD/vbird/VbIrD 等等的不需要的字串,而造成使用者的困擾。

再舉個系統常見的例子好了,假設妳發現系統在開機的時候,老是會出現一個關於 mail 程式的錯誤, 而開機過程的相關程序都是在 /etc/rc.d/ 底下,也就是說,在該目錄底下的某個檔案內具有 mail 這個關鍵字,好了,此時,您怎麼找出來含有這個關鍵字的檔案??您當然可以一個檔案一個檔案的開啟, 然後去搜尋 mail 這個關鍵字,只是.....該目錄底下的檔案可能不止 100 個說~ 如果瞭解正規表示法的相關技巧,那麼只要一行指令就找出來啦! 『grep 'mail' /etc/rc.d/*』 那個 grep 就是支援正規表示法的工具程式之一!如何~很簡單吧! ^_^y

談到這裡就得要進一步說明了,正規表示法基本上是一種『表示法』, 只要工具程式支援這種表示法,那麼該工具程式就可以用來作為正規表示法的字串處理之用。 也就是說,例如 vi, grep, awk ,sed 等等工具,因為她們有支援正規表示法, 所以,這些工具就可以使用正規表示法的特殊字元來進行字串的處理。

小標題的圖示 正規表示法對於系統管理員的用途
那麼為何我需要學習正規表示法呢?對於一般使用者來說,由於使用到正規表示法的機會可能不怎麼多, 因此感受不到他的魅力,不過,對於身為系統管理員的您來說, 正規表示法則是一個『不可不學的好東西!』 怎麼說呢?由於系統如果在繁忙的情況之下,每天產生的訊息資訊會多到你無法想像的地步, 而我們也都知道,系統的『 錯誤訊息登錄檔案』 的內容(這部份我們在第五篇會詳談)記載了系統產生的所有訊息,當然, 這包含你的系統是否被『入侵』的紀錄資料。

但是系統的資料量太大了,要身為系統管理員的你每天去看這麼多的訊息資料, 從千百行的資料裡面找出一行有問題的訊息,呵呵~光是用肉眼去看,想不瘋掉都很難! 這個時候,我們就可以透過『正規表示法』的功能,將這些登錄的資訊進行處理, 僅取出『有問題』的資訊來進行分析,哈哈!如此一來,你的系統管理工作將會 『快樂得不得了』啊!當然,正規表示法的優點還不止於此,等您有一定程度的瞭解之後,您會愛上他喔!

小標題的圖示 正規表示法的廣泛用途
正規表示法除了可以讓系統管理員管理主機更為便利之外,事實上, 由於正規表示法強大的字串處理能力,目前一堆軟體都支援正規表示法呢! 最常見的就是『郵件伺服器』啦!

如果您留意網際網路上的消息,那麼應該不能發現,目前造成網路大塞車的主因之一就是『垃圾/廣告信件』了, 而如果我們可以在主機端,就將這些問題郵件剔除的話,用戶端就會減少很多不必要的頻寬耗損了。 那麼如何剔除廣告信件呢?由於廣告信件幾乎都有一定的標題或者是內容,因此, 只要每次有來信時,都先將來信的標題與內容進行特殊字串的比對,發現有不良信件就予以剔除! 嘿!這個工作怎麼達到啊?就使用正規表示法啊!目前兩大伺服器軟體 sendmail 與 postfix 以及支援郵件伺服器的相關分析套件,都支援正規表示法的比對功能!

當然還不止於此啦,很多的伺服器軟體、以及套件都支援正規表示法呢!當然, 雖然各家軟體都支援他,不過,這些『字串』的比對還是需要系統管理員來加入比對規則的, 所以啦!身為系統管理員的你,為了自身的工作以及用戶端的需求, 正規表示法實在是很需要也很值得學習的一項工具呢!

小標題的圖示 正規表示法與 Shell 在 Linux 當中的角色定位
說實在的,我們在學數學的時候,一個很重要、但是粉難的東西是一定要『背』的, 那就是九九乘法表,背成功了之後,未來在數學應用的路途上,真是一帆風順啊! 這個九九乘法表我們在小學的時候幾乎背了一整年才背下來,並不是這麼好背的呢! 但他卻是基礎當中的基礎!您現在一定受惠相當的多呢 ^_^! 而我們談到的這個正規表示法,與前一章的 BASH shell 就有點像是數學的九九乘法表一樣,是 Linux 基礎當中的基礎,雖然也是最難的部分, 不過,如果學成了之後,一定是『大大的有幫助』的!這就好像是金庸小說裡面的學武難關, 任督二脈,打通任督二脈之後,武功立刻成倍成長!所以啦, 不論是對於系統的認識與系統的管理部分,他都有很棒的輔助啊!請好好的學習這個基礎吧! ^_^

小標題的圖示 延伸的正規表示法
正規表示法除了簡單的一組字串處理之外,還可以作群組的字串處理, 例如進行搜尋 VBird 或 netman 或 lman 的搜尋,注意,是『或(or)』而不是『和(and)』的處理, 此時就需要延伸正規表示法的幫助啦!藉由特殊的 ( 與 | 等字元的協助, 就能夠達到這樣的目的!好啦!清清腦門,咱們用功去囉!

大標題的圖示基礎正規表示法

既然正規表示法是處理字串的一個標準表示方式,他需要支援的工具程式來輔助, 所以,我們這裡就先介紹一個最簡單的字串擷取功能的工具程式,那就是 grep 囉! 在介紹完 grep 的基本功能之後,就進入正規表示法的特殊字符的處理能力了。


小標題的圖示 以 grep 擷取字串
既然要使用 grep 當然就得要先瞭解一下 grep 的語法囉~
[root@test root]# grep [-acinv] '搜尋字串' filename
參數說明:
-a :將 binary 檔案以 text 檔案的方式搜尋資料
-c :計算找到 '搜尋字串' 的次數
-i :忽略大小寫的不同,所以大小寫視為相同
-n :順便輸出行號
-v :反向選擇,亦即顯示出沒有 '搜尋字串' 內容的那一行!
範例:
[root@test root]# grep 'root' /var/log/secure
將 /var/log/secure 這個檔案中有 root 的那一行秀出來

[root@test root]# grep -v 'root' /var/log/secure
若該行沒有 root 才將資料秀出來到螢幕上!

[root@test root]# last | grep root
若該行有 root 才將資料秀出來到螢幕上!
grep 是一個很常見也很常用的指令,他最重要的功能就是進行字串資料的比對, 然後將符合使用者需求的字串列印出來。 需要說明的是『 grep 在資料中查尋一個字串時,是以 "整行" 為單位來進行資料的擷取的!』也就是說,假如一個檔案內有 10 行,其中有兩行具有你所搜尋的字串,則將那兩行顯示在螢幕上,其他的就丟棄了!

而 grep 除了可以進行檔案的資料搜尋之外,也常常被應用在 input/output 的資料處理當中,例如常見的 管線命令 ( pipe ) 就可以常常見到他的蹤影! 以上面表格中的例子來看,我們可以發現前兩個例子是查尋檔案的內容,有沒有加上 -v 所顯示出來的結果是『相反的!』,而第三個例子則是以 pipe 的功能進行資料的處理的喔!

好了,我們就開始以 grep 來進行正規表示法的簡易說明吧!我們先以底下這個檔案來作為範例:
[root@test root]# vi regular_express.txt
"Open Source" is a good mechanism to develop programs.
apple is my favorite food.
Football game is not use feet only.
this dress doesn't fit me.
However, this dress is about $ 3183 dollars.
GNU is free air not free beer.
Her hair is very beauty.
I can’t finish the test.
Oh! The soup taste good.
motorcycle is cheap than car.
This window is clear.
the symbol '*' is represented as start.
Oh! My god!
The gd software is a library for drafting programs.
You are the best is mean you are the no. 1.
The world is the same with "glad".
I like dog.
google is the best tools for search keyword.
goooooogle yes!
go! go! Let's go.
# I am VBird

需要特別注意的是,上面這個檔案鳥哥是在 Windows 的環境下編輯的, 並且經過特殊處理過,因此,他雖然是純文字檔,但是內含一些 Windows 環境下的軟體常常自行加入的一些特殊字元,例如斷行字元(^M)就是一例! 所以,您可以直接將上面的文字以 vi 儲存成 regular_express.txt 這個檔案, 不過,比較建議直接點底下的連結下載: 此外,因為不同的語系編碼是不一樣的,所以,您必須要將語系改成英文語系, 才能夠進行底下的測試,否則,可能會有顯示的內容與底下的輸出不符的狀況喔! 修改語系的方法為:
[root@test root]# LANG=en
[root@test root]# LANGUAGE=en
好了,現在開始我們一個案例一個案例的來介紹吧!
  • 例題一、搜尋特定字串:
    搜尋特定字串很簡單吧?假設我們要從剛剛的檔案當中取得 the 這個特定字串, 最簡單的方式就是這樣:
    [root@test root]# grep -n 'the' regular_express.txt
    8:I can't finish the test.
    12:the symbol '*' is represented as start.
    15:You are the best is mean you are the no. 1.
    16:The world  is the same with "glad".
    18:google is the best tools for search keyword.
    
    那如果想要『反向選擇』呢?也就是說,當該行沒有 'the' 這個字串時,才顯示在螢幕上,那就直接使用:
    [root@test root]# grep -vn 'the' regular_express.txt
    
    您會發現,螢幕上出現的行列為除了 8,12,15,16,18 五行之外的其他行列! 接下來,如果您想要取得不論大小寫的 the 這個字串,則:
    [root@test root]# grep -in 'the' regular_express.txt
    8:I can't finish the test.
    9:Oh! The soup taste good.
    12:the symbol '*' is represented as start.
    14:The gd software is a library for drafting programs.
    15:You are the best is mean you are the no. 1.
    16:The world  is the same with "glad".
    18:google is the best tools for search keyword.
    
  • 例題二、利用 [] 來搜尋集合字元
    如果我想要搜尋 test 或 taste 這兩個單字時,可以發現到,其實她們有共通的 't?st' 存在~這個時候,我可以這樣來搜尋:
    [root@test root]# grep -n 't[ae]st' regular_express.txt
    8:I can't finish the test.
    9:Oh! The soup taste good.
    
    瞭解了吧?其實 [] 裡面不論有幾個字元,他都謹代表某『一個』字元, 所以,上面的例子說明了,我需要的字串是『tast』或『test』兩個字串而已! 而如果想要搜尋到有 oo 的字元時,則使用:
    [root@test root]# grep -n 'oo' regular_express.txt
    1:"Open Source" is a good mechanism to develop programs.
    2:apple is my favorite food.
    3:Football game is not use feet only.
    9:Oh! The soup taste good.
    18:google is the best tools for search keyword.
    19:goooooogle yes!
    
    但是,如果我不想要 oo 前面有 g 的話呢?此時,可以利用在集合字元的反向選擇 [^] 來達成
    [root@test root]# grep -n '[^g]oo' regular_express.txt
    2:apple is my favorite food.
    3:Football game is not use feet only.
    18:google is the best tools for search keyword.
    19:goooooogle yes!
    
    意思就是說,我需要的是 oo ,但是 oo 前面不能是 g 就是了! 仔細比較上面兩個表格,妳會發現,第 1,9 行不見了,因為 oo 前面出現了 g 所致! 第 2,3 行沒有疑問,因為 foo 與 Foo 均可被接受!但是第 18 行明明有 google 的 goo 啊~ 別忘記了,因為該行後面出現了 tool 的 too 啊!所以該行也被列出來~ 也就是說, 18 行裡面雖然出現了我們所不要的項目 (goo) 但是由於有需要的項目 (too) , 因此,是符合字串搜尋的喔!

    至於第 19 行,同樣的,因為 goooooogle 裡面的 oo 前面可能是 o ,例如: go(ooo)oogle ,所以,這一行也是符合需求的!

    再來,假設我 oo 前面不想要有小寫字元,所以,我可以這樣寫 [^abcd....z]oo , 但是這樣似乎不怎麼方便,由於小寫字元的 ASCII 上編碼的順序是連續的, 因此,我們可以將之簡化為底下這樣:
    [root@test root]# grep -n '[^a-z]oo' regular_express.txt
    3:Football game is not use feet only.
    
    也就是說,當我們在一組集合字元中,如果該字元組是連續的,例如大寫英文/小寫英文/數字等等, 就可以使用[a-z],[A-Z],[0-9]等方式來書寫,那麼如果我們的要求字串是數字與英文呢? 呵呵!就將他全部寫在一起,變成:[a-zA-Z0-9]

    例如,我們要取得有數字的那一行,就這樣:
    [root@test root]# grep -n '[0-9]' regular_express.txt
    5:However, this dress is about $ 3183 dollars.
    15:You are the best is mean you are the no. 1.
    
    這樣對於 [] 以及 [^] 以及 [] 當中的 - 有瞭解了嗎?! ^_^y
  • 例題三、行首與行尾字元 ^ $:
    我們在例題一當中,可以查詢到一行字串裡面有 the 的,那如果我想要讓 the 只在行首列出呢? 這個時候就得要使用定位字元了!我們可以這樣做:
    [root@test root]# grep -n '^the' regular_express.txt
    12:the symbol '*' is represented as start.
    
    此時,就只剩下第 12 行,因為只有第 12 行的行首是 the 開頭啊~此外, 如果我想要開頭是大寫字元的那一行就列出呢?可以這樣:
    [root@test root]# grep -n '^[a-z]' regular_express.txt
    2:apple is my favorite food.
    4:this dress doesn't fit me.
    10:motorcycle is cheap than car.
    12:the symbol '*' is represented as start.
    18:google is the best tools for search keyword.
    19:goooooogle yes!
    
    如果我不想要開頭是英文字母,則可以是這樣:
    [root@test root]# grep -n '^[^a-zA-Z]' regular_express.txt
    1:"Open Source" is a good mechanism to develop programs.
    20:# I am VBird
    
    注意到了吧?那個 ^ 符號,在字元集合符號(括號[])之內與之外是不同的! 在 [] 內代表『反向選擇』,在 [] 之外則代表定位在行首的意義!要分清楚喔!

    那如果我想要找出來,行尾結束為小數點 (.) 的那一行,該如何處理:
    [root@test root]# grep -n '/.$' regular_express.txt
    1:"Open Source" is a good mechanism to develop programs.
    2:apple is my favorite food.
    3:Football game is not use feet only.
    4:this dress doesn't fit me.
    10:motorcycle is cheap than car.
    11:This window is clear.
    12:the symbol '*' is represented as start.
    15:You are the best is mean you are the no. 1.
    16:The world  is the same with "glad".
    17:I like dog.
    18:google is the best tools for search keyword.
    
    特別注意到,因為小數點具有其他意義(底下會介紹),所以必須要使用跳脫字元(/)來加以解除其特殊意義! 不過,您或許會覺得奇怪,但是第 5~9 行最後面也是 . 啊~怎麼無法列印出來?? 這裡就牽涉到 Windows 平台的軟體對於斷行字元的判斷問題了!我們使用 cat -A 將第五行拿出來看, 您會發現:
    [root@test root]# cat -A regular_express.txt
    However, this dress is about $ 3183 dollars.^M$
    
    注意到了沒?最後面的斷行字元應該是 $ 才對,但是,因為 Windows 的 nodepad 會主動加上 ^M 作為斷行的判斷,因此,那個 . 自然就不是緊接在 $ 之前喔!這樣可以瞭解 ^ 與 $ 的意義嗎? 好了,先不要看底下的解答,自己想一想,那麼如果我想要找出來,哪一行是『空白行』, 也就是說,該行並沒有輸入任何資料,該如何搜尋??
    [root@test root]# grep -n '^$' regular_express.txt
    21:
    
    因為只有行首跟行尾( ^$ ),所以,這樣就可以找出空白行啦!再來, 假設您已經知道在一個批次腳本 (shell script) 或者是設定檔當中, 空白行與開頭為 # 的那一行是註解,因此如果您要將資料列出給別人參考時, 可以將這些資料省略掉,以節省保貴的紙張,那麼,您可以怎麼作呢? 我們以 /etc/syslog.conf 這個檔案來作範例,您可以自行參考一下輸出的結果:
    [root@test root]# cat /etc/syslog.conf
    [root@test root]# grep -v '^$' /etc/syslog.conf | grep -v '^#'
    
    是否節省很多版面啊??
  • 例題四、任意一個字元 . 與重複字元 *
    在 bash 的章節當中,我們知道萬用字元 * 可以用來代表任意(0或多個)字元, 但是正規表示法並不是萬用字元,兩者之間是不相同的! 至於正規表示法當中的『 . 』則代表『絕對有一個任意字元』的意思!這樣講不好懂, 我們直接做個練習吧!假設我需要找出 g??d 的字串,亦即共有四個字元, 起頭是 g 而結束是 d ,我可以這樣做:
    [root@test root]# grep -n 'g..d' regular_express.txt
    1:"Open Source" is a good mechanism to develop programs.
    9:Oh! The soup taste good.
    16:The world  is the same with "glad".
    
    因為強調 g 與 d 之間一定要存在兩個字元,因此,第 13 行的 god 與第 14 行的 gd 就不會被列出來啦!再來,如果我想要列出有 oo, ooo, oooo 等等的資料, 也就是說,至少要有兩個 o 以上,該如何是好??是 o* 還是 oo* 還是 ooo* 呢? 雖然您可以試看看結果, 不過結果太佔版面了 @_@ ,所以,我這裡就直接說明。

    因為 * 代表的是『重複 0 個或多個前面的 RE 字符』的意義, 因此,『o*』代表的是:『擁有空字元或一個 o 以上的字元』, 特別注意,因為允許空字元(就是有沒有字元都可以的意思),因此, grep -n 'o*' regular_express.txt 將會把所有的資料都列印出來螢幕上!

    那如果是『oo*』呢?則第一個 o 肯定必須要存在,第二個 o 則是可有可無的多個 o , 所以,凡是含有 o, oo, ooo, oooo 等等,都可以被列出來~

    同理,當我們需要『至少兩個 o 以上的字串』時,就需要 ooo* ,亦即是:
    [root@test root]# grep -n 'ooo*' regular_express.txt
    1:"Open Source" is a good mechanism to develop programs.
    2:apple is my favorite food.
    3:Football game is not use feet only.
    9:Oh! The soup taste good.
    18:google is the best tools for search keyword.
    19:goooooogle yes!
    
    這樣理解 * 的意義了嗎?!好了,現在出個練習,如果我想要字串開頭與結尾都是 g, 但是兩個 g 之間僅能存在至少一個 o ,亦即是 gog, goog, gooog.... 等等, 那該如何?
    [root@test root]# grep -n 'go*g' regular_express.txt
    18:google is the best tools for search keyword.
    19:goooooogle yes!
    
    如此瞭解了嗎?好,再來一題,如果我想要找出 g 開頭與 g 結尾的字串, 當中的字元可有可無,那該如何是好?是『g*g』嗎?
    [root@test root]# grep -n 'g*g' regular_express.txt
    1:"Open Source" is a good mechanism to develop programs.
    3:Football game is not use feet only.
    9:Oh! The soup taste good.
    13:Oh!  My god!
    14:The gd software is a library for drafting programs.
    16:The world  is the same with "glad".
    17:I like dog.
    18:google is the best tools for search keyword.
    19:goooooogle yes!
    
    但測試的結果竟然出現這麼多行??太詭異了吧? 其實一點也不詭異,因為 g*g 裡面的 g* 代表『空字元或一個以上的 g』 在加上後面的 g ,因此,整個 RE 的內容就是 g, gg, ggg, gggg , 因此,只要該行當中擁有一個以上的 g 就符合所需了!

    那該如何得到我們的 g....g 的需求呢?呵呵!就利用任意一個字元『.』啊! 亦即是:『g.*g』的作法,因為 * 可以是 0 或多個重複前面的字符,而 . 是任意字元,所以: 『.* 就代表零個或多個任意字元』的意思啦!
    [root@test root]# grep -n 'g.*g' regular_express.txt
    1:"Open Source" is a good mechanism to develop programs.
    14:The gd software is a library for drafting programs.
    18:google is the best tools for search keyword.
    19:goooooogle yes!
    
    因為是代表 g 開頭與 g 結尾,中間任意字元均可接受,所以,第 1 與第 14 行是可接受的喔! 這個 .* 的 RE 表示任意字元是很常見的,希望大家能夠理解並且熟悉!

    再出一題,如果我想要找出『任意數字』的行列呢?因為僅有數字,所以就成為:
    [root@test root]# grep -n '[0-9][0-9]*' regular_express.txt
    5:However, this dress is about $ 3183 dollars.
    15:You are the best is mean you are the no. 1.
    
    雖然使用 grep -n '[0-9]' regular_express.txt 也可以得到相同的結果, 但鳥哥希望大家能夠理解上面指令當中 RE 表示法的意義才好!

    那麼,如果我想要一個字串裡面開頭是 g 而結尾是 d 時,
  • 例題五、限定連續 RE 字符範圍 {}
    在上個例題當中,我們可以利用 . 與 RE 字符及 * 來設定 0 個到無線多個重複字元, 那如果我想要限制一個範圍區間內的重複字元數呢?舉例來說,我想要找出兩個到五個 o 的連續字串,該如何作?這時候就得要使用到限定範圍的字符 {} 了。 但因為 { 與 } 的符號在 shell 是有特殊意義的,因此, 我們必須要使用跳脫字符 / 來讓他失去特殊意義才行。

    至於 {} 的語法是這樣的,假設我要找到兩個 o 的字串,可以是:
    [root@test root]# grep -n 'o/{2/}' regular_express.txt
    1:"Open Source" is a good mechanism to develop programs.
    2:apple is my favorite food.
    3:Football game is not use feet only.
    9:Oh! The soup taste good.
    18:google is the best tools for search keyword.
    19:goooooogle yes!
    
    這樣看似乎與 ooo* 的字符沒有什麼差異啊?因為第 19 行有多個 o 依舊也出現了! 好,那麼換個搜尋的字串,假設我們要找出 g 後面接 2 到 5 個 o ,然後再接一個 g 的字串, 他會是這樣:
    [root@test root]# grep -n 'go/{2,5/}g' regular_express.txt
    18:google is the best tools for search keyword.
    
    嗯!很好!第 19 行終於沒有被取用了(因為 19 行有 6 個 o 啊!)。 那麼,如果我想要的是 2 個 o 以上的 goooo....g 呢?除了可以是 gooo*g ,也可以是:
    [root@test root]# grep -n 'go/{2,/}g' regular_express.txt
    18:google is the best tools for search keyword.
    19:goooooogle yes!
    
    呵呵!就可以找出來啦~

小標題的圖示 重要特殊字元(characters)
經過了上面的幾個簡單的範例,我們可以將基礎的正規表示法特殊字符彙整如下:

RE 字符意義與範例
^word待搜尋的字串(word)在行首!
範例:grep -n '^#' regular_express.txt
搜尋行首為 # 開始的那一行!
word$待搜尋的字串(word)在行尾!
範例:grep -n '!$' regular_express.txt
將行尾為 ! 的那一行列印出來!
.代表『任意一個』字符,一定是一個任意字符!
範例:grep -n 'e.e' regular_express.txt
搜尋的字串可以是 (eve) (eae) (eee) (e e), 但不能僅有 (ee) !亦即 e 與 e 中間『一定』僅有一個字元,而空白字元也是字元!
/跳脫字符,將特殊符號的特殊意義去除!
範例:grep -n /' regular_express.txt
搜尋含有單引號 ' 的那一行!
*重複零個或多個的前一個 RE 字符
範例:grep -n 'ess*' regular_express.txt
找出含有 (es) (ess) (esss) 等等的字串,注意,因為 * 可以是 0 個,所以 es 也是符合帶搜尋字串。另外,因為 * 為重複『前一個 RE 字符』的符號, 因此,在 * 之前必須要緊接著一個 RE 字符喔!例如任意字元則為 『.*』 !
/{n,m/}連續 n 到 m 個的『前一個 RE 字符』
若為 /{n/} 則是連續 n 個的前一個 RE 字符,
若是 /{n,/} 則是連續 n 個以上的前一個 RE 字符!
範例:grep -n 'go/{2,3/}g' regular_express.txt
在 g 與 g 之間有 2 個到 3 個的 o 存在的字串,亦即 (goog)(gooog)
[]字元集合的 RE 特殊字符的符號
[list]
範例:grep -n 'g[ld]' regular_express.txt
搜尋含有 (gl) 或 (gd) 的那一行~
需要特別留意的是,在 [] 當中『謹代表一個待搜尋的字元』,
例如: a[afl]y 代表搜尋的字串可以是 aay, afy, aly
亦即 [afl] 代表 a 或 f 或 l 的意思!


[ch1-ch2]
範例:grep -n '[0-9]' regular_express.txt
搜尋含有任意數字的那一行!需特別留意,在字元集合 [] 中的減號 - 是有特殊意義的,他代表兩個字元之間的所有連續字元!但這個連續與否與 ASCII 編碼有關, 因此,您的編碼需要設定正確(在 bash 當中,需要確定 LANG 與 LANGUAGE 的變數是否正確!) 例如所有大寫字元則為 [A-Z]

[^]
範例:grep -n 'oo[^t]' regular_express.txt
搜尋的字串可以是 (oog) (ood) 但不能是 (oot) ,那個 ^ 在 [] 內時, 代表的意義是『反向選擇』的意思~例如,我不要大寫字元,則為 [^A-Z] ~ 但是,需要特別注意的是,如果以 grep -n [^A-Z] regular_express.txt 來搜尋, 卻發現該檔案內的所有行都被列出,為什麼?因為這個 [^A-Z] 是『非大寫字元』的意思, 因為每一行均有非大寫字元,例如第一行的 "Open Source" 就有 p,e,n,o.... 等等的小寫字元, 以及雙引號 (") 等字元,所以當然符合 [^A-Z] 的搜尋!

請特別留意的是,『 正規表示法的特殊字元』 與一般在指令列輸入指令的『 萬用字元』並不相同, 例如,在萬用字元當中,* 代表的是 0 ~ 無限多個字元的意思,但是在正規表示法當中, * 則是重複 0 到多個的前一個 RE 字符的意思~使用的意義並不相同,不要搞混了! (鳥哥我一開始摸正規表示法時就很容易搞混!因為這裡是新手最容易搞錯的地方,特別小心啊!)

舉例來說,不支援正規表示法的 ls 這個工具中,若我們使用 『ls -l * 』 代表的是任意檔名的檔案,而 『ls -l a* 』代表的是以 a 為開頭的任何檔名的檔案, 但在正規表示法中,我們要找到含有以 a 為開頭的檔案,則必須要這樣:(需搭配支援正規表示法的工具)
  • ls | grep -n '^a.*'
這樣是否瞭解正規表示法與萬用字元的差異啦??

大標題的圖示延伸正規表示法

事實上,一般讀者只要瞭解基礎型的正規表示法大概就已經相當足夠了,不過,某些時刻, 為了要簡化整個指令操作,瞭解一下使用範圍更廣的延伸型正規表示法的表示式,會更方便呢! 舉個簡單的例子好了,在上節的例題三的最後一個例子中,我們要去除空白行與行首為 # 的行列, 使用的是
  • grep -v '^$' regular_express.txt | grep -v '^#'
需要使用到管線命令來搜尋兩次! 那麼如果使用延伸型的正規表示法,我們可以簡化為:
  • egrep -v '^$|^#' regular_express.txt
利用支援延伸型正規表示法的 egrep 與特殊字元 | 來區隔兩組字串,如此一來,是否方便很多呢?

這裡必須要特別強調, grep 支援的是基礎型的正規表示法,而 egrep 支援延伸正規表示法。 事實上, egrep 是 grep -E 的命令別名,為了方便使用,我們還是以 egrep 來跟 grep 區分吧!

熟悉了正規表示法之後,到這個延伸型的正規表示法,您應該也會想到, 不就是多幾個重要的特殊符號嗎? ^_^y 是的~所以,我們就直接來說明一下,延伸型正規表示法有哪幾個特殊符號?

RE 字符意義與範例
+重複『一個或一個以上』的前一個 RE 字符
範例:egrep -n 'go+d' regular_express.txt
搜尋 (god) (good) (goood)... 等等的字串。 那個 o+ 代表『一個以上的 o 』所以,上面的執行成果會將第 1, 9, 13 行列出來。
?『零個或一個』的前一個 RE 字符
範例:egrep -n 'go?d' regular_express.txt
搜尋 (gd) (god) 這兩個字串。 那個 o? 代表『空的或 1 個 o 』所以,上面的執行成果會將第 13, 14 行列出來。
有沒有發現到,這兩個案例( 'go+d' 與 'go?d' )的結果集合與 'go*d' 相同? 想想看,這是為什麼喔! ^_^
|用或( or )的方式找出數個字串
範例:egrep -n 'gd|good' regular_express.txt
搜尋 gd good 這兩個字串,注意,是『或』! 所以,第 1,9,14 這三行都可以被列印出來喔!那如果還想要找出 dog 呢?就這樣啊:
egrep -n 'gd|good|dog' regular_express.txt
( )找出『群組』字串
範例:egrep -n 'g(la|oo)d' regular_express.txt
搜尋 (glad) 或 (good) 這兩個字串,因為 g 與 d 是重複的,所以, 我就可以將 la 與 oo 列於 ( ) 當中,並以 | 來分隔開來,就可以啦!
此外,這個功能還可以用來作為『多個重複群組』的判別喔!舉例來說:
echo 'AxyzxyzxyzxyzC' | egrep 'A(xyz)+C'
上面的例子當中,意思是說,我要找開頭是 A 結尾是 C ,中間有一個以上的 "xyz" 字串的意思~
 

以上這些就是延伸型的正規表示法的特殊字元。另外,要特別強調的是,那個 ! 在正規表示法當中並不是特殊字元, 所以,如果您想要查出來檔案中含有 ! 與 > 的字行時,可以這樣:
  • grep -n '[!>]' regular_express.txt
這樣可以瞭解了嗎?!常常看到有陷阱的題目寫:『反向選擇這樣對否? '[!a-z]'?』, 呵呵!是錯的呦~要 '[^a-z] 才是對的!
未完~待續!

 


 

  •  

重點回顧

  • 使用 grep 或其他工具進行正規表示法的字串比對時,因為編碼的問題會有不同的狀態,因此, 您最好將 LANG 及 LANGUAGE 等變數設定為 C 或者是 en 等英文語系!
  • 正規表示法 ( Regular Expression ) 的用途主要是用來做為『搜尋』字串之用,還可以用來過濾特殊訊息等用途;
  • 由於嚴謹度的不同,正規表示法之上還有更嚴謹的延伸正規表示法;
  • 正規表示法的處理方式,經常是以『整行』或稱為『整段』來進行處理的;
  • grep 與 egrep 在正規表示法裡面是很常見的兩支程式,其中, egrep 支援更嚴謹的正規表示法的語法;

本章與 LPI 的關係


參考資源:


本章習題練習 ( 要看答案請將滑鼠移動到『答:』底下的空白處,按下左鍵圈選空白處即可察看 )

  • 例題:我想要知道某個檔案裡面含有 boot 的字眼,而這個檔案在 /etc/ 底下,我要如何找出這個檔案?
    答:
    • 既然知道有這個字眼那就好辦了!可以直接下達:
      grep boot /etc/*
       
  • 題:我想要知道,在 /etc 底下,只要含有 XYZ 三個字元的任何一個字元的那一行就列出來,要怎樣進行?
    答:
    • 『只要』含有 X 或 Y 或 Z 就將該行列出來,因此,我們的範圍很很廣啦!這個時候就必需要使用到 [] 這個咚咚!還記得中括號的用途嗎?那就是『在中括號裡面謹代表一個字元而已!』而這個中括號是一個『代表』,可以是一串字也可以是幾個不連續的字!這裡我們僅需要 XYZ 其中任何一個,所以可以這樣寫:
      grep [XYZ] /etc/*
      則只要在每一行當中,只要發現 X 或 Y 或 Z 任何一個,就會將他印出來!這個與 grep XYZ /etc/* 是『完全不一樣』的!請仔細的思考一下ㄟ!
       
  • 例題:我想要找出在 /etc 底下,檔案內容含有 * 的檔案名稱?
    答:
    • 由於 * 是特殊字元,在變數的訂定法則裡面曾經提過要將特殊字元移除,需要使用跳脫字元,亦即是 / 符號,所以我可以這樣下達指令:
      grep /* /etc/*
                  •  

                     

                =====================================================================

                正規表示式的入門與應用(一)
                張耀仁, changyj@rtfiber.com.tw
                現任職瑞泰纖維工業 資訊部
                恆逸資訊 RHCE 講師
                曾任美商網虎國際教育認證部顧問
                譯作『Linux 核心研究篇』
                本文原文刊登於 Linuxer 第三期 pp.166-175
                歡迎連結, 如欲轉載請與作者聯絡

                 

                前言

                近幾年來, 在圖形介面的影響下, 許多有效率, 功能強大的命令列工具逐漸為人所淡忘。似乎管理系統是件只要按按滑鼠就能完成的簡單工作。事實上, 圖形化的管理程式固然讓初學者容易上手, 但想要有效率地管理系統, 而不是被系統所牽制, 還是應該學習一些『命令列型』(command line)的工具。在 Linux/Unix 中, 許多命令列型的工具, 例如 grep, sed, perl 等擁有強大的字串處理能力;在使用這些工具時, 我們可以利用簡潔且相當有彈性的『正規表示式』(regular expression) 來指定要搜尋的字串, 然後做進一步的處理。雖說正規表示式本身屬於正規語言(formal language)的範疇, 感覺似乎蠻嚴肅的, 其實要應用在實務上並不困難。在本文中, 我們採用從做中學的方式把正規表示式介紹給各位。

                利用 grep 來搜尋字串

                講到搜尋字串, 有經驗的讀者想必馬上聯想到 grep, egrep 與 fgrep 這三個兄弟(註:這三個字串所成的集合可以用 [ef]?grep 這個正規表示式來描述, 以後會加以說明)。其中 grep 與 egrep 分別支援基本型(basic)與延伸型(extended)的正規表示式。 我們從基礎學起, 因此先採用 grep。

                在學習如何使用 grep 之前, 我們先製作一個測試用的文字檔, data1: (讀者也可以用 vi, joe 等編輯器來製作)

                [changyj:~] cat > data1
                劃底線的部分是讀者應輸入的內容, 在每一行結束時請按 Enter, 往後的範例中也是如此。 he can't hear the noises
                made by the bear
                near the pear tree in
                the rear garden since he
                is deaf in one ear

                (按 CTRL-D 結束)

                註:筆者所用的 shell 是 bash, 提示符號的格式是
                [使用者名稱:現行工作目錄], 讀者可以執行
                export PS1='[/u:/w] '
                把提示符號換成相同的格式。
                [changyj:~] cat data1 接著檢查檔案內容
                he can't hear the noises
                made by the bear
                near the pear tree in
                the rear garden since he
                is deaf in one ear

                如果我們想知道 data1 這個檔案內是否有 hear 這個字串時, 可以利用 grep 這個工具程式, grep 會把檔案中含有 hear 這個字串的每一行列出來:

                [changyj:~] grep -n 'hear' data1
                1:he can't hear the noises

                說明

                 

                grep 發現第一行有 hear 這個字串, 因此把它列出來。
                由於加了 -n 選項, grep 會同時顯示該行是第幾行

                指令格式

                 

                grep '欲搜尋的字串' 檔名 或
                grep 一或多個選項 '欲搜尋的字串' 檔名
                選項 -n: 要求 grep 在輸出時同時列出行號
                 -i: 把大小寫字母視為相同
                如果不指定檔名時, grep 會從標準輸入讀取資料


                那麼 grep 是如何發現第一行中有 hear 這個字串的呢我們採用下圖來說明:

                首先把欲搜尋的字串(以下簡稱為『字模』, pattern)與該行的第一個字元對齊。我們把資料行中字模內第一個字元相重疊的位置稱為『比較基準點』。在比較第一組相重疊的字元時我們發現兩者相同, 這代表 hear 這個字串有可能在比較基準點出現, 因此, 我們繼續比較下一個字元。

                由於第二組相重疊的字元相同, 因此字串 hear 仍有可能出現在目前的比較基準點。我們繼續比較下一個字元。

                第三組相重疊的字元並不相同:一為空白而另一為 a, 可知字串 hear 不可能出現在目前的比較基準點, 我們把字模往右移一個字元(比較基準點亦隨之移動)。

                第一組相重疊的字元並不相同, 因此字串 hear 不可能出現在當前的比較基準點, 把字模再往右移一個字元。
                在接下來的幾個比較基準點中, 第一組相重疊的字元都不相同, 我們持續把字模向右移, 一直到以下的位置時:

                發現第一組相重疊的字元相同, 因此繼續比較第二組, 以此類推。

                當比較完第四組時, 發現欲字模中已無尚未比較的字元, 因此 grep 判定目前的比較基準點出現了 hear 字串, 於是把該行列出。
                (註:以上所呈現的搜尋方式稱為 brute-force search, 即暴力搜尋法。而在考量執行效率時, 程式設計師會採用其他更有效率但較艱深的方法, grep 也是如此。這裡以暴力法來說明是為讓讀者容易了解, 並不會影響搜尋結果的正確性)

                 

                建構正規表示式

                讀到這邊, 讀者可能想為何還沒見到正規表示式(以下有時將簡稱為 RE)呢? 實際上各位已經與 RE 做過第一次接觸了, 在讀完以下說明後就能明白。在上例中, 我們欲搜尋的字串是 hear, 只要是 h 之後緊接著 e, 再接著 a, 再接著 r 就對了, 至於出現在資料行的那個位置則不重要。雖然在比較的過程中我們會把字模 hear 一次往右移一個位置, 但字模 hear 內 h, e, a, r 四個字元之間的相對位置則是固定的, 下表中的『第幾個字元』所描述的就是這種相對位置。我們先列出下表:

                字模內的
                第幾個字元
                1234
                允許值h e a r

                接著利用上表來建構一個只有 hear 這個字串能符合的 RE:首先為每個位置建構 RE。基本上, RE 允許我們限定某個位置是
                1. 某一個字元,
                2. 某些字元,
                3. 除了某些字元之外的字元, 或是
                4. 任意字元
                現在遇到的是第一種情形。要限制只能出現字元 h 的 RE 要怎麼寫呢? 寫法就是 h。依此類推, e, a, r 也是如此, 我們得到
                字模內的
                第幾個字元
                1234
                允許值h e a r
                對應的 REh e a r

                把這四個位置對應的 RE 依序連接(concatenate)起來成為一個更大的 RE, hear:而只有字串 hear 能符合 hear 這個 RE。事實上, 在 grep -n 'hear' data1 這個命令列中, grep 是把 hear 視為一個正規表示式, 當某一行出現符合該 RE 的字串時, 就把該行列出來。因此, grep 指令格式應該解釋為

                grep '正規表示式' 檔名



                 

                字元集合的應用-特殊字元 [ 及 ]

                在接下來的單元中, 我們將讓各位逐漸體認正規表示式的『威力』。當遇到一些有共通特性的字串時, 我們常可用一個 RE 來描述這些字串所成的集合, 而不必逐一列舉。舉例來說, 如果要檢查 data1 這個檔案內是否有 bear 或是 rear 這個字串時, 我們發現這兩個字串有個共通的特性:除了第一個字元外其餘都相同。仿照前例列出下表:

                字模內的
                第幾個字元
                1234
                允許值b 或 r e a r

                接著按『允許值』為每個位置建構 RE。其中, 第二, 三, 四個位置的寫法與前例相同。而第一個位置上允許 b 或 p 兩個字元, 其相對應的 RE 是 [br], 其中 [ 及 ] 是特殊字元(metacharacter), 可用來描述字元集合(character class), 代表只有在這一對 [ ] 內的字元才是被允許的。而[br] 也可以寫成 [rb], 因為符合這兩個 RE 的字元是相同的。我們把上表再加上每個位置所對應的 RE 得到
                字模內的
                第幾個字元
                1234
                允許值b 或 r e a r
                對應的 RE[br] e a r

                把這四個位置的 RE 連接起來, 我們得到 [br]ear 這個 RE, 只有 bear 以及 rear 符合這個 RE。接著試試看看效果如何:

                [changyj:~] grep -n '[br]ear' data1
                2:made by the bear
                4:the rear garden since he


                果然把含有 bear 或 rear 的那幾行列出來了。

                 

                反相字元集合-[^...]

                如果想看看除了 bear, rear 以外, 是否有和它們一樣都是以 ear 結尾的字串呢? 跟先前一樣, 把需求列表如下:

                字模內的
                第幾個字元
                1234
                允許值除了 b 或 r
                之外的任何字元
                e a r

                由於除了 b, r 之外的字元實在太多, 若要把這些字元全列在一個字元集合內未免太不實際;因而 RE 的語法中提供了一種特殊的反相字元集合(negated character class):只有不列在其中的字元才符合該 RE。例如只有 b, r 之外的字元才符合 [^br] 這個 RE。有了這種反相字元集合, 我們所要的 RE 就是 [^br]ear, 測試一下:

                [changyj:~] grep -n '[^br]ear' data1
                1:he can't hear the noises
                3: near the pear tree in
                5:is deaf in one ear

                說明
                1. data1 的第 1 行被列出來是因為該行的 hear 符合 [^br]ear 這個 RE。
                2. 在檢查第 3 行時, 雖然 near 與 pear 都符合 [^br]ear, 但因為 grep 先碰到 near 並在發現它符合 [^br]ear 時 就把該行列出來, 而不再檢查下去, 因此實際上 grep 並不知道該行還有 pear 也是符合的。
                3. 只要是 b, r 以外的字元都符合 [^br]ear, 當然空白字元 (whitespace, 如空格, TAB 等)也符合這個 RE。 因此 ' ear' 符合 [^br]ear, 這就是為什麼第 5 行會被列出來的原因。


                最後, 如果想看看那幾行中含有 r 之後接著任意一個字元, 再接著 e 的字串時該怎麼辦? RE 中提供了 . 這個特殊字元:任何字元都符合 . 這個 RE。因此, 我們所要的 RE 就是r.e, 測試一下:

                [changyj:~] grep -n 'r.e' data1
                3:near the pear t ree in
                4:the rear ga rden since he


                問題 1

                在 HTML 檔案中常含有像 <H1>, ..., <H6>, 或 <h1>, ..., <h6>等這種表頭標籤, 現在想把檔案中含有這些表頭標籤的資料行列出來應怎麼做?


                做法

                 

                相信讀者對如何建構 RE 已經有了基本的概念, 因此可以輕鬆地列出下表:
                字模內的
                第幾個字元
                1234
                允許值< H 或 h 1, 2, 3, 4, 5,
                6 中的任何一個
                >
                對應的 RE< [Hh] [123456] >

                我們把每個位置的 RE 結合起來得到 /<[Hh][123456]/>, 但其中 [123456] 這個部分不僅寫的人累, 看的人也煩。恰巧 1 到 6 這些字元在 ASCII 上編碼的順序是連續的;
                而在反相字元集合與反相字元集合中, 若允許值內含有在 ASCII 編碼上連續出現的字元時, 我們可以僅列出這些字元中編碼值最小與最大者, 兩者用 - 號連接即可。因此 [123456] 可以寫成 [1-6];而 0 到 9 的數字(digit)可寫成 [0-9];大, 小與小大寫字母分別可寫成 [A-Z], [a-z], [A-Za-z], [a-zA-Z]。讀者可以試試
                grep -n '<[Hh][1-6]>' HTML檔 的效果如何。


                從本文開始迄今, 在執行 grep 時, 我們都在正規表示式的前後加上單引號。這樣做的好處是當 RE 中含有 shell 的特殊字元, 像本例中的輸出入轉向字元 < 及 > 時, shell 會把 RE 原封不動地傳給 grep 而不會把它支解掉。

                 

                定位字元-^ 與 $

                在前面幾個例子中, 我們只要求 grep 去尋找符合 RE 的字串, 並沒有限定字串在資料行中的位置。事實上在 RE 中, 我們可以指定字串必須出現在

                1. 行首,
                2. 行尾, 或
                3. 某個特定位置。


                針對第(1)及(2)項, 我們有兩個特殊的定位(anchoring)字元可用, ^ 與 $:

                • 如果在 RE 前加上 ^ 號, 代表不僅字串必須符合該 RE, 還必須位於行首才行;
                • 如果在 RE 後加上 $ 號, 那麼字串必須位於行尾才行。


                我們做個實驗:

                [changyj:~] cat | grep -n '^test'
                ---test (輸入第一行)
                test (輸入第二行)
                2:test (由於第二行中 test 位於行首, 符合 RE 的要求, 因此被列出來)
                -----test (輸入第三行)
                test (輸入第四行)
                4:test (理由同第二行)
                (按 CTRL-D 結束)

                說明
                1. 使用 cat 不指定檔名時, cat 會從鍵盤(標準輸入)讀取資料然後送往 標準輸出;而使用 grep 不指定檔名時, 它會從標準輸入讀取資料。 在本實驗中, 我們透過管線(pipe)符號 | 把 cat 的標準輸出與 grep 的標準輸入連結起來, 因此 grep 處理的正是我們鍵入的資料。
                2. 只有第二及第四次輸入的 test 是位於該行的最前端, 才符合 ^test, 因此 grep 把它們列出。


                讀者可自行實驗特殊字元 $:

                [changyj:~] cat | grep -n 'foo$'
                ---foo test (輸入第一行)
                ---test foo (輸入第二行)
                2:---test foo (因為 foo 在一行的尾端, 符合 RE, 因此被列出來)
                (按 CTRL-D 結束)


                而第(3)項要求在處理上則稍微複雜一些。例如想看看檔案 data1 內有那幾行從第 6 個字元開始是 the 這個字串時, 我們應如何設計對應的 RE?

                一行的第
                幾個字元
                行首 1 2 3 4 5 6 7 8
                允許值 任意字元 t h e
                對應的 RE^ . . . . . t h e

                把每個位置的 RE 依序連接起來得到 ^.....the 這個 RE。由於這個 RE 最前端有 ^ 號, 因此在進行比較搜尋時, 比較基準點會固定在資料行的第一個位置, 這是與前面幾個例子不同之處。試試看效果如何:

                [changyj:~] grep -n '^.....the' data1
                3:near the pear tree in


                在下一期的內容中, 我們將繼續探討這個問題。

                問題 2

                請讀者想想要怎樣的資料行才符合

                1. ^test$
                2. ^$
                3. ^.....$
                這三個 RE?
                說明
                1. 整行只含有 test 這個字串的行。
                2. 完全不含任何字元的空行。
                3. 整行不多不少, 只由 5 個字元所構成的行。


                 

                有無均可, 多一點也可以的 * 字元

                相信讀者都知道, 在 shell 下執行 ls a* 會把現行目錄下所有名稱以 a 開頭的物件(檔案, 子目錄)列出來, 其中 * 的部分可以是一個或多個任意的字元, 也可以不含任何字元。在正規表示式中, * 也是個特殊字元, 但它必須與它前方只佔一個位置的 RE 合用。舉例來說, 在 ab*c 這個 RE 中, b* 是一體的, 其中 b, bb, ..., 及連續多個 b 都符合 b* 這個 RE, 如果連一個 b 也沒有也可以;因此, 符合 ab*c 這個 RE 的字串有 ac, abc, abbc, ... 等, 也就是 a 之後為任意個 b (一個都沒有也可以)再接個 c。嚴格來說, 如果 X 是一個 RE, 在 X, XX, XXX, ..., 等這些由 n 個連續的 X 所連接成的 RE 中, 一個字串只要符合其中一個, 或它是不佔任何空間, 不含任何字元的空字串, 都視為符合 X* 這個 RE。做個實驗:

                [changyj:~] echo 'abbbc' | grep -n 'ab*c'
                1:abbbc


                grep 是如何知道 abbbc 符合 ab*c 這個 RE 呢? 在比對 b* 這個部分, 當 grep 發現有 b 存在時, 會儘可能把多個連續的 b 劃為 b* 的範圍, 因此會把 abbbc 內的 bbb 全納入 b* 的範圍;由於這項特性, 我們稱 * 為貪心(greedy)的特殊字元。那麼 ac 這個字串呢?

                [changyj:~] echo 'ac' | grep -n 'ab*c'
                1:ac


                在嘗試比對 b* 這個部分, 當 grep 發現並沒有 b 緊接在 a 之後時, 它會認為 a 與 c 之間藏有一個不含任何字元的空字串;由於空字串符合 b*, 因此繼續比對 c 這個部分。最後 grep 認為 ac 符合 ab*c 而把該行列出來。

                問題 3

                 

                在問題 1 中, 我們嘗試在 HTML 格式的檔案中, 找出 <H1>, ..., <H6> 等標題。如果在數字之後與 > 之間允許空格的存在時, 應如何改寫原來的 RE 呢?

                做法

                首先嘗試寫出『一個或多個空格, 而沒有也可以符合』的 RE: *(為求清晰, 有些地方以 來表示空格); 也可以寫成 [ ]* 。原來的 RE 應改寫成 <[Hh][1-6] *>。
                實驗看看:
                [changyj:~] echo '<h1 >test</h1>' | grep -n '<[Hh][1-6]>'
                [changyj:~]
                (先測試原來的 RE。grep 沒有輸出直接回到 shell 提示符號, 代表未找到合乎 RE 的字串)

                [changyj:~] echo '<h1 >test</h1>' | grep -n '<[Hh][1-6] *>'
                1: <h1 >test</h1>
                [changyj:~] echo '<H2>another</H2>' | grep -n '<[Hh][1-6] *>'
                1: <H2>another</H2>
                在最後一個執行例子中, 2 與 > 之間並沒有任何字元, 我們可以想像這兩者之間藏了一個空字串。由於空字串符合 *, 因此 <H2> 符合 <[Hh][1-6] *> 而被列出來。


                問題 4

                 

                除了空字串之外, 有那些字串符合 [0-9]* 呢?

                說明

                 

                讀者先想想有那些字串符合 [0-9] 及 [0-9][0-9]?
                1. 0, 1, ..., 9 等字串符合 [0-9]。
                2. 在符合 [0-9][0-9] 的字串中, 第一及第二個字元各可以是 0, 1, ..., 9 中的任何一個; 這兩個字元彼此獨立, 因此, 00, 01, ..., 09, 10, ..., 99 等字串 都符合, 而不是只有 00, 11, 22, ..., 99 等十個字串符合而已。
                3. 由於長度為 n 的數字符合由連續 n 個 [0-9] 連接而成的 RE, 而這個 RE 屬於 [0-9]* 的一員; 因此, 任意長度的數字都符合 [0-9]* 這個 RE, 其中也包含具有前導零(leading zeros)的字串, 如 00123, 00045 等。

                問題 5

                 

                在使用 * 時, 由於空字串符合 RE*, 有時會造成不便(稍後會加以說明)。在上例中, 如果要把空字串排除在 [0-9]* 之外, 也就是只讓長度至少為 1 的數字符合時, 應如何改寫 RE?

                說明

                 

                我們所要的字串至少要有一位數(最左邊);至於第二位及以上則非必要。將需求列表如下:
                字模內的
                第幾個字元
                12 3 4 ...
                允許值0 至 9 的數字 0 至 9 的數字
                但可有可無
                對應的 RE[0-9] [0-9]*

                把兩個部分的 RE 組合起來得到 [0-9][0-9]*。測試看看:


                [changyj:~] echo 'no number' | grep -n '[0-9][0-9]*'
                [changyj:~]
                (直接回到 shell 的提示符號, 代表 grep 並未找到合乎 RE 的字串)

                [changyj:~] echo 'a number 123456' | grep -n '[0-9][0-9]*'
                1:a number 123456 (找到了!!)


                問題 6

                 

                想看看一個 C 程式中, 有那些地方出現了以 0X 或 0x 開頭的十六進位數字, 像 0xABC 或 0X1bC 時應怎麼辦?

                說明

                 

                在 0X 或 0x 之後, 起碼要有一位數;在十六進位的數字中, 每一位數必須是 0 到 9, A 到 F, 或 a 到 f 間任一字元, 這一部分可以利用 [0-9A-Fa-f] 這個字元集合來描述。建構 RE 的方法類似上題, 列表如下:
                字模內的
                第幾個字元
                1234 5 6...
                允許值0 X 或 x 0 到 9
                A 到 F
                a 到 f
                間的任一字元
                同左邊,
                但可有可無
                對應的 RE0 [Xx] [0-9A-Fa-f] [0-9A-Fa-f]*
                [Xx][0-9A-Fa-f][0-9A-Fa-f]* 是我們所要的 RE。


                在進入下個單元之前, 我們把到目前為止介紹過的特殊字元整理一下。一個字元是否為特殊字元與它是否在字元集合內有很大的關係, 用兩個表格來說明:

                 

                1. 字元集合內, 被視為特殊字元的有
                  字元意義使其成為一般字元的方法
                  ^會把字元集合反相 不放在 [ 之後的第一個位置
                  -用來指定範圍, 如 [a-z] 放在 [ 或 [^ 之後第一個位置
                  ]視為字元集合的結束 放在 [ 或 [^ 之後第一個位置
                2. 字元集合外, 被視為特殊字元的有
                  字元意義使其成為一般字元的方法
                  任意的字元都符合 在字元前加上反斜線 /
                  ^代表行首
                  $代表行尾
                  *與其前方只佔一個位置的 RE 配合
                  請參考前面的說明
                  [代表字元集合的開始
                  /使特殊字元失去特殊意義
                sed 的基本應用

                在 Linux 下, sed (stream editor)是一個很有用的命令列型工具, 它採用與 grep 相同的 RE 語法;我們可以利用它讀入檔案的內容加以編輯, 其中最常應用到的功能是『取代』, 我們用一個例子來說明:

                [changyj:~] cat > data2 (建立一個測試用的資料檔)
                I have a cat.
                The cat is gray.
                (按 CTRL-D)
                [changyj:~] sed -e 's/cat/dog/' data2 (把檔案中的 cat 字串換成 dog)
                I have a dog.
                The dog is gray.


                sed 在執行時會以行為單位, 依序對檔案的每一行執行以下的步驟:

                1. 把該行的內容讀到一緩衝區(buffer)內, sed 稱該區域為 pattern space。
                2. 對 pattern space 的內容執行編輯命令。 我們利用 -e 選項來指定編輯命令, 在本例中的編輯命令為 s/dog/cat/, 其意義是把符合 'dog' 這個 RE 的字串換成 cat。 通常我們會在編輯命令的前後加上一對單(雙)引號。
                3. 把編輯過後的內容送到標準輸出。

                從這些執行步驟可以發現, sed 並未修改 data2 這個資料檔的內容。如果『真的』要修改檔案內容, 我們可以把 sed 的輸出轉向到一個暫存檔, 再利用暫存檔把原來的檔案覆蓋掉。例如:

                [changyj:~] sed -e 's/cat/dog/' data2 > tmpfile; mv tmpfile data2

                注意

                暫存檔名不可與原資料檔相同路徑與檔名;否則會原資料檔的內容會消失


                在使用 sed 時如果不指定檔名, sed 會從標準輸入來讀取資料, 我們可以利用這種特性來測試所設計的 RE 是否正確:

                [changyj:~] echo 'I have a cat.' | sed -e 's/cat/dog/'
                I have a dog.
                說明
                1. 由於未指定檔名, sed 會從標準輸入讀取資料。
                2. 由於管線(|)的緣故, sed 的標準輸入被連接到 echo 的標準輸出。
                3. 因此 sed 讀到的資料是 I have a cat. 執行編輯命令後, 把結果, 也就是 I have a dog. 輸出。


                再試試看:

                [changyj:~] echo 'I have a cat. The cat is cute.' | sed -e 's/cat/dog/'
                I have a dog. The cat is cute.


                為什麼第二個 cat 字串仍然沒有改變呢? 事實上, 當 sed 在進行取代的動作時, 在沒有特別指定的情形下, 它只會把讀進 pattern space 的資料行中, 第一個符合 RE 的字串取代掉。如果要全部取代, 我們必須在編輯命令的末端加上 g, 代表 global 之意。這樣一來, 一行中所有符合 cat 這個 RE 的字串都會被換成 dog 了。

                [changyj:~] echo 'I have a cat. The cat is cute.' | sed -e 's/cat/dog/g'
                I have a dog. The dog is cute.


                問題 7

                如果想在每一行的
                1. 行首加上 ===>> 的字樣
                2. 末尾加上 <<=== 的字樣時
                3. 上述 (1) 與 (2) 同時加在每一行上時應怎麼做?

                做法

                要做到 (1) 與 (2), 必須利用定位字元 ^ 與 $, 而所要編輯命令分別是 s/^/===>>/s/$/<<===/

                而 (3) 則需花一點工夫。首先想想, 什麼樣的 RE 可以代表一整行? 其實我們可以把一整行看成:行首後接著零, 一或多個任意個字元, 再接著是行尾;若用 RE 來表示就是 ^.*$。因此, 所要的編輯命令是 s/^.*$/===>>&<<===/

                其中在『用來取代的字串』中, & 是個特殊符號, 用來引用 sed 所找到之符合 RE 的字串;在本例中, & 代表的是符合 ^.*$ 的字串, 也就是一整行的內容。
                實驗看看:
                [changyj:~] echo 'line for test' | sed -e 's/^.*$/===>>&<<===/'
                ===>>line for test<<===


                必須留意的陷阱(1)
                陷阱 1

                想把 It indicates that cat is too fat. 這個句子中, cat 這個單字換成 dog:
                [changyj:~] echo 'It indicates that cat is too fat.' | sed -e 's/cat/dog/g'
                It indi doges that dog is too fat.

                結果把 indicates 內的 cat 字串也換掉了。由於 sed 是 cat 當成 RE, 而不是像我們有單字的觀念, 當它碰到符合的字串就取代掉了。『目前』解決的方法是要求 sed 只把前後各有一個空格的 cat 取代掉, 把 RE 從 cat 改成 cat :
                [changyj:~] echo 'It indicates that cat is too fat.' | sed -e 's/ cat / dog /g'
                It indicates that dog is too fat.


                陷阱 2

                在問題 5 中曾說空字串有時候會造成不便。例如我們要搜尋是否有任意長度(至少為 1 位數)的數字, 如果使用 [0-9]* 來做為 RE 時, 會出現奇特的現象:
                [changyj:~] echo 'a-123' | grep -n '[0-9]*'
                1:a-123
                [changyj:~] echo 'no-number' | grep -n '[0-9]*'
                1:no-number

                看起來似乎只有第二個測試結果有問題。由於 sed 與 grep 採用相同的 RE 語法, 我們可以利用 sed 在符合 [0-9][0-9]* 以及 [0-9]* 之字串的前後各加上 < 及 >, 這樣可清楚地看出是哪些字串符合: [changyj:~] echo 'a-123' | sed -e 's/[0-9][0-9]*/<&>/'
                a-<123>

                在『用來取的字串』中, <&> 代表要在合乎 RE 之字串的前後各加上 < 及 >。從結果發現, 若使用 [0-9][0-9]* 不會有問題。但是...
                [changyj:~] echo 'a-123' | sed -e 's/[0-9]*/<&>/'
                <>a-123

                咦? 為什麼會發生這種情形? 由於在比對是否有字串符合 [0-9]* 的過程中, 當 sed, grep 發現 a 並不屬於 [0-9] 時, 會認為行首與 a 之間有一個空字串, 由於空字串符合 [0-9]*, 因此 grep 會把該行列出﹑sed 會執行取代命令而在空字串前後各加上 < 及 >。而這也是為什麼 'no-number' 雖不含任何數字, 但仍被列出來的原因。
                •  
                  1. 這項工作可用 sed 的取代命令 s 來處理﹐其中利用了 ^ 這個定位字元﹐它代表行首﹐也就是第一個字元的前方。 編輯命令

                    s/^//

                    會在該位置加入三個空格而達到所要求的效果:

                    由於管線符號(|)之後必須有 shell 命令﹐ 因此上例中在 | 後直接按 ENTER 時﹐shell 認為尚未結束而顯示 > 這個提示符號要求我們繼續輸入;當然這兩行也可以合併成一行。

                     

                  2. 雖然要把字串『刪除』﹐我們仍然可以利用 s 命令來完成: 只須把要刪除的字串用不含任何字元的『空字串』來『取代』即可﹐格式為

                    s/正規表示式//g

                    在『用來取代的字串』這個部分﹐我們以不輸入任何字元的方式來代表空字串。 例如要把每一行中﹐所有的 abc 刪除掉﹐利用 s/abc//g 這個命令即可:

                    在知道如何『刪除』字串後﹐ 請讀者想想只讓 一行前端一或多個空格 能符合的 RE 該怎麼寫?分成兩個部分:

                    • 一行前端:利用定位字元 ^﹐代表字串不僅必須符合 ^ 之後的 RE﹐ 還得在一行的前端才行。
                    • 一或多個空格: 請參考下表

                      條件對應的 RE
                      零或多個空格*
                      一個(含)以上的空格*
                      兩個(含)以上的空格*

                    把這兩個部分結合得到

                    ^*

                    這個 RE。 請注意﹐如果不加上 ^ 這個定位字元﹐而又加了 g 這個選項時﹐ 一行中所有的空格都會被刪掉﹐這是讀者應當留意的。實驗看看:

                     

                  3. 從前一題的表格中得知﹐只讓 兩個(含)以上的空格 符合的 RE 是 * 因此

                    s/*//g

                    就是我們所要的編輯命令:

                    (註:用 s/*//g 也可以達到相同的效果﹐ 但意義不同﹐請讀者想想看)

                  1. 把每行第 10 個字元起的資料清除掉。
                  2. 在每行第 9 個字元之後加入 HERE 這個字串。
                  1. 把該行的資料載入 pattern space。
                  2. 對 pattern space 依序執行命令1﹑命令2﹑...﹑命令n。
                    • 在上期的說明中﹐在這個步驟內只有一個編輯命令。
                    • 要執行命令x 之前﹐sed 會檢查資料行是否符合命令x 的位址部分﹐若是則執行編輯命令;若否則跳過不執行。
                  3. 把 pattern space 的內容輸出。
                  1. 前面曾提到 s/xx/yy/g 是『把資料行內所有的 xx 換成 yy』﹐ 如果只對資料行執行一個命令時﹐這種說法是正確的; 而當對資料行執行多個命令時﹐上述說法應調整成

                    『把當時 pattern space 內﹐所有的 xx 換成 yy』

                    在問題 11 的圖示中﹐很清楚的可以看到

                    • 第 1 個命令 s/^*// 編輯的是原始的資料。
                    • 第 2 個命令 s/*//g 編輯的是經過第 1 個命令 s/^*// 編輯過的內容。
                    因此﹐命令x(x>1)所編輯的內容是依序經過 命令1﹑...﹑命令x-1 編輯過的內容﹐在撰寫多個命令的命令檔時應留意。
                  2. /RE/ 是否成立也是由進行決策時當時 pattern space 的內容來決定。 因此

                     

                    更嚴謹的說法是

                    『若此時 pattern space 內含有字串 yes﹐則把所有的 xx 換成 yy』

                     

                  • 刪除某些字串
                  • 取用符合 RE 字串的某些部分
                  • 定址-限制 sed 只對某些資料行執行編輯命令
                  • 位址與正規表示式的結合
                  • 刪除資料行
                  • 不對某些資料行執行編輯命令
                  • 字的邊界
                  • 限制符合正規表示式的次數
                  • 對一資料行執行多個編輯命令
                  •  
                    1. 在每一行的最前端(以下簡稱為『行首』)加上 3 個空格。
                    2. 把行首所有的空格﹐不論多少個﹐一律刪除。
                    3. 把每一行中﹐所有兩個(含)以上的空格以一個空格代替。
                  • 請利用 sed 完成以下的工作:
                  • 我們常用 mm-dd-yy (月-日-年)這種格式來表示日期; 如果要把文字檔中﹐原為 mm-dd-yy 的日期格式轉換成 yy-mm-dd (年-月-日)時 該怎麼辦呢?
                  • 為求簡單起見﹐我們假設要處理的檔案中不會出現 23-55-99 這種不合理的日期;因此﹐ 我們可以把符合 mm-dd-yy 這種格式的所有字串用

                    [01][0-9]-[0-3][0-9]-[0-9][0-9]

                    這個 RE 來描述﹐它的意義表列如下:

                    欲搜尋字串的
                    笫幾個字元
                    12﹑5﹑
                    7﹑8
                    43﹑6
                    允許值0 或 10 到 90 到 3-

                    接著利用一對對的 /( 及 /) 分別把代表年﹑月﹑日的部分標記起來﹐ 把上述的 RE 改寫成

                    /([01][0-9]/)-/([0-3][0-9]/)-/([0-9][0-9]/)

                    觀察這個新出爐的 RE 內共有三組由 /( 及 /) 所構成的部分﹐ 組別的編號是從左至右﹐從一開始算起:

                    組別內容說明如何取用符合 RE
                    的字串的該部分
                    1[01][0-9]屬於『月』/1
                    2[0-3][0-9]屬於『日』/2
                    3[0-9][0-9]屬於『年』/3

                    用來取代的字串是 年-月-日﹐ 其中代表年﹑月﹑日的字串分別是第 3﹑1﹑及第 2 組用 /( 及 /) 標記起來的部分﹐ 要取用這部分的字串時﹐分別用 /3﹑/1 及 /2 來代表; 因此﹐我們把用來取代的字串寫成

                    /3-/1-/2

                    把這兩個部分組合成 s 命令:

                    s//([01][0-9]/)-/([0-3][0-9]/)-/([0-9][0-9]/)//3-/1-/2/g

                    試試看對不對呢?

                     

                  • 不指定位址(0-位址):sed 會對每一行執行該編輯命令。
                  • 有一個位址(1-位址):sed 只會對行號等於該位址的資料行執行編輯命令。
                  • 有兩個位址(2-位址):sed 只會對行號在該二位址間的資料行執行編輯命令。
                  • /yes/ 為位址的部分﹐代表一個資料行中必須含有 yes 這個字串 才是我們所要的。
                  • s/xx/yy/g 是編輯命令﹐用來把一行中所有的(所以最後加了個 g 選項) xx 換成 yy。
                  • 左大括弧 { 後要直接按 ENTER﹐不可以有任何其他的字元。
                  • 右大括弧 } 必須是一行的第一個字元。
                  • 組合 1- 行號 X, /RE/
                    指定的範圍從第 X 行起﹐到第 X 行後第一個含有符合 RE 字串的資料行為止。

                    在上述這個例子中﹐第 2 行之後含有 yes 字串的是第 5 行﹐因此 sed 只會把從第 2 行起﹐到第 5 行為止的 xx 換成 yy。

                    為了能理解 sed 處理 2-位址 的方式﹐讀者不妨想像有一個開關﹐而 on 與 off 是由兩個位址來把關的﹐當在 on 的狀態時﹐sed 就執行位址後的編輯命令;而在 off 狀態時則否:

                    • sed 在從資料檔讀入第 1 行之前﹐開關是處在 off 的狀態。
                    • 如果開關是處在 off 狀態﹐ 當碰到一個符合第一個位址的資料行時﹐sed 會把開關切為 on。
                    • 從開關被切為 on 的下一行起﹐當 sed 碰到第一個符合第二個位址 的資料行時﹐會在對該行執行完編輯命令後把開關切為 off。 之後若有資料行符合第一個位址時(在組合 3 中有範例說明)﹐ 仍會再把開關切為 on。
                    • 對於同一個開關﹐一資料行只能把它由 on 切到 off﹐或由 off 切到 on﹐ 而不會同時執行。
                    因此﹐在上例中﹐開關在第 2 行時被切為 on﹐而在處理完第 5 行後被切為 off。

                    但如果第 2 行之後﹐都沒有任何一行含有 yes 字串時會怎樣呢?由於開關不會被切到 off﹐因此 sed 會對第 2 行起到最後一行止﹐每一行執行 s/xx/yy/g 的命令:

                     

                  • 組合 2-/RE/, 行號 X
                    所指定的範圍為從第一個含有符合 RE 字串的資料行起到第 X 行為止。

                    在這個例子中﹐第一個含有 no 的資料行是第 3 行﹐因此開關在第 3 行被切為 on﹐一直要到最後一行($)處理完畢後才會被切為 off。

                  • 組合 3-/RE1/, /RE2/
                    可用來處理單純的(非巢狀,non-nested)區塊﹐我們用例子來說明:

                    [說明]

                    • 在處理第 1 行之前﹐開關處於 off 的狀態。
                    • 第 2 行含有 begin﹐因而 /begin/ 成立﹐開關切為 on﹐從現在起﹐ sed 會對每一行執行 s/xxxx/yyyy/g 的命令; 而到第 5 行時﹐由於含有 end﹐因而 /end/ 成立﹐處理完第 5 行後開關被切到 off;請注意﹐如果第 5 行含有 xxxx﹐一樣會被換成 yyyy。
                    • 到第 7 行時開關又被切為 on﹐一直到第 9 行才再被切為 off。
                  • 為了能容易辨識出符合 10/{2,4/}1 的字串﹐我們在找到的字串前後各加上 < 與 >﹐因此在 s 命令中﹐把『用來取代的字串』這個部分寫成 <&>﹐其中 & 在此處 代表符合 10/{2,4/}1 這個 RE 的字串。
                  •  
                    1. 很明顯的﹐我們必須把一資料行分成兩個部分來處理:
                      • 前半由前 9 個字元所構成﹐ 這部分利用『字串的再利用』保留下來。
                      • 後半由第 10 個字元起的資料所構成﹐ 藉由用空字串取代的方式把這部分清除掉。
                      而起碼有 9 個字元的資料行可以用

                      ^..........*$^./{9/}.*$ 來描述

                      (註:由於 * 有 greedy 的特性﹐把 RE 寫成 ^./{9/}.* 也可以)。

                      而要保留前 9 個字元﹐我們把它們利用一對 /(/) 圈起來﹐ RE 變成了:

                      ^/(./{9/}).*$

                      而編輯命令 s/^/(./{9/}).*$//1/ 則保留前 9 個字元﹐而把之後的字元刪除。

                    2. 利用編輯命令 s/^./{9/}/&HERE/ 即可。
                  • 我們可以利用編輯命令 s 的取代功能來進行刪除與加入字串的工作。
                  • 在問題 8 的(2)與(3)中分別介紹了如何把行首的空格刪掉﹐以及把多個空格用一個空格代替﹐現在請把這兩項工作結合在一起。
                  • 如果要先把行首的空格刪掉﹐再把多個空格用一個空格代替時﹐ 命令檔的內容應為

                    下圖為 sed 對

                    這行資料執行命令檔的過程:

                  • Sed & Awk 第二版
                    由 Dale Dougherty & Arnold Robbins 合著﹐ O'Reilly and Associates 出版﹐這本書前半部描述 RE 的觀念﹑ sed 基本以及進階命令的介紹﹐堪稱為 sed 的聖經﹐適合初學者入門建立觀念 與進階者進修。
                  • SED 常問問題集
                    http://www.cornerstonemag.com/sed/sedfaq.html 這份文件內容有
                    • sed 的基本介紹(2.1 節)
                    • 不同平台上的 sed(2.2 節)
                    • 參考書籍(2.3.1 節)﹑教學文件的網址(2.3.3 節)﹑ 線上相關資源(2.3.4 節)
                    • sed 進階使用(第 3 章)
                    • 範例 (第 4 章)
                    • 疑難解答(第 5 章)
                    • 其他
                    內容相當豐富﹐值得讀者參考。

                • 正規表示式的入門與應用(二)

                  張耀仁
                  E-mail: changyj@rtfiber.com.tw
                  http://www.rtfiber.com.tw/~changyj
                  刊登於 Linuxer 第四期 pp.152-161
                  作者同時譯有 Linux 核心研究篇
                  著作權所有﹐如欲轉載請與筆者聯絡
                  *筆者整理了一些與 sed 相關的資源放在網頁
                  *謝謝馬兒(marr@xlinux.com) 先生的潤稿

                  在上期的內容中﹐我們介紹了正規表示式(regular expression﹐簡稱 RE)的基本觀念﹐並利用 grep 與 sed 示範一些 RE 的用途。本期的重點在於介紹 sed 的編輯命令﹐包含了

                  (註:上一期經修正後的版本請到 Linuxers 或筆者的網頁中閱讀)

                  首先筆者提出一個具有實用性的問題﹐一方面讓讀者複習上期的內容﹐另一方面則介紹如何『刪除』某些字串(為讓讀者易於解讀﹐在某些地方我們會以 來表示空格):

                  [問題 8]

                  [說明]

                  取用符合 RE 字串的某些部分-/( 與 /) 的運用

                  在 sed 的取代命令 s 內『用來取代的字串』的這個部分﹐我們可以利用特殊字元 & 來取得符合 RE 的字串;如果我們要的只是該字串的某些部分時﹐可以先在 RE 中把這些部分對應的位置『標記』起來﹐然後在『用來取代的字串』內加以引用。用一個例子來說明:

                  [問題 9]

                  [說明]

                  定址-限制 sed 只對某些資料行執行編輯命令

                  到目前為止﹐在所介紹的範例中﹐sed 會對檔案中每一行的內容執行編輯命令。事實上﹐我們可以要求 sed 只對某些資料行(如第二行﹐第五行)執行命令﹐我們用例子逐一說明。

                  首先製作測試檔 data3﹐內容如下:

                  編輯命令 s/xx/yy/g 可以把每一行中的 xx 都換成 yy:

                  如果只要把第二行內所有的 xx 換成 yy 時﹐執行

                  即可﹐其中編輯命令前的 2 代表第二行﹐如果是 3 則代表第三行﹐依此類推;符號 $ 在此有特殊的意義--用來代表檔案的最後一行:

                  我們也可以要求 sed 只對某個範圍內(從第幾行起到第幾行為止)的資料行﹐執行把 xx 換成 yy 的命令:

                  編輯命令前的 3,$ 代表只對第三行起到最後一行為止的資料行執行該編輯命令。

                  在上述的例子中﹐指定第二﹑三及最後一行時所用的 2﹑3 與 $﹐ sed 稱它們為位址(address)﹐而要編輯命令只執行在某些資料行的動作稱為定址(addressing)。sed 在執行編輯命令之前﹐會先判斷目前處理的資料行是否屬於位址所指定的範圍。在前幾個例子中﹐我們看到編輯命令前的位址格式有三種:

                  在往後有關 sed 的說明中﹐所提到的『命令』是由位址(可以是 0,1,2-位址)以及編輯命令兩個部分結合而成的。

                  位址與正規表示式的結合

                  在上面幾個例子中﹐我們指定位址的方式不外乎是數字或是符號 $。另有一種更有彈性的方式是

                  /正規表示式/

                  若應用在 1-位址 的位址格式下﹐只有當一行中含有符合該正規表示式的字串時﹐ sed 才會對該行執行編輯命令。例如

                  /yes/s/xx/yy/g

                  這個命令含了兩個部分:

                  因此 sed 處理這個命令的方式是只對含有 yes 的資料行執行 s/xx/yy/g 的動作﹐觀察以下的執行結果:

                  但這種寫法解讀起來有些不方便﹐我們可以利用一組大括弧把命令中的位址與編輯命令分開來:

                  讀者應注意:

                  另外﹐我們可以把編輯命令存在檔案中﹐再指定 sed 到該檔案中擷取命令: 首先把

                  存入 script 這個檔案中﹐然後執行

                  sed -f script data3

                  即可得到相同的結果。其中選項 -f 後為命令檔(script file)的名稱。

                  把正規表示式應用在 2-位址 的位址格式上

                  /正規表示式/ 也可以應用在兩個位址的格式上﹐一共有三種組合:

                  刪除資料行-編輯命令 d

                  編輯命令 d 可用來刪除資料行﹐它必須與位址合用才有意義;例如執行

                  時﹐sed 沒有輸出任何資料而直接回到 shell 的提示符號﹐因為 sed 每讀進一行﹐我們就要求它把該行刪掉﹐當然就沒有資料可以輸出了。

                  而命令 /yes/d 可以把含有 yes 字串的資料行刪除:

                  所有含有 yes 字串的資料行都被刪掉了。 如果要把空行刪掉呢?在編輯命令 d 前加上只有空行能符合的條件﹐也就是 /^$/ 即可:

                   

                  不對某些資料行執行編輯命令

                  剛才我們學會了利用位址來要求 sed 只對某些資料行執行編輯命令;如果在位址與編輯命令之間加入 !﹐ sed 會對不屬於該位址的資料行執行編輯命令;例如

                  /yes/!d

                  會把不含有 yes 字串的資料行刪除掉:

                   

                  字的邊界

                  在上一期中﹐我們曾提到找出 cat 這個英文單字的『暫時性』解法﹐是把 RE 設計成

                  cat

                  也就是 cat 前後各有一個空格才行。這樣雖然可以找出

                  It indicates that cat is too fat.

                  that 之後的 cat 而不是夾在 indicates 中的 cat﹐但這個解法並無法找出

                  I have a cat.

                  中的單字 cat。在設計更好的 RE 之前先想想看﹐當要找 cat 這個『單字』時﹐為什麼我們能一眼分辨出在這兩個句子中﹐真正的單字 cat 呢?原因是真正的單字 cat 前後並不會緊接著英文字母。根據這項推論﹐我們可以設計出新的 RE:

                  [^A-Za-z]cat[^A-Za-z]

                  試試看效果如何?

                  我們發現有些單字 cat 並沒有被找出來;事實上﹐單字 cat 在一行中的位置可歸類為四種:

                  組別單字 cat 的分佈情形有效的 RE符合左方 RE
                  的字串的長度
                  1只由 cat 這三個字元
                  所構成的資料行
                  ^cat$3
                  2位在一行的開頭^cat[^A-Za-z]4
                  3位在一行的結尾[^A-Za-z]cat$4
                  4一行的中間[^A-Za-z]cat[^A-Za-z]5

                  剛才所想的新 RE 只能找出屬於第 4 組的 cat;看來要找出單字 cat 還真是件不容易的事。還好在 GNU 版本的 grep 以及 sed 內提供了 /b 這個特殊序列(meta-sequence)﹐用來代表字的邊界(word boundary)﹐它是一種不佔空間的定位字元;有了它﹐要找出以上四種位置的 cat 就可以用

                  /bcat/b

                  這個 RE 來解決:

                  (註:grep 與 sed 的 /b 所設定的條件比前表所列的更嚴格﹐我們採用的是 [^A-Za-z]﹐而它們採用的是 [^A-Za-z0-9]

                  限制符合的次數

                  在上一期的內容中曾提到 * 這個特殊字元﹐在它之前必須有一個佔有一個位置的 RE;如果 X 是這樣的 RE﹐一個字串只要符合由連續 n 個 X 所結合成的 RE﹐如 X, XX, XXX, ... 等的任何一個﹐或它是空字串﹐就算符合 X* 這個 RE。

                  而在基本型 RE 中﹐我們可以利用 /{/} 可用來限制符合 X 的次數;與 * 相同﹐它們也必須與佔有一個位置的 RE 共用。以下的表格將列出 /{/} 的用法:

                  格式字串須符合怎樣的 RE
                  才合乎要求
                  X/{n/}由連續 n 個 X 所構成
                  X/{n,m/}連續 n 到 m 個 X
                  X/{n,/}至少連續 n 個 X
                  • X 為佔有一個位置的 RE
                  • n 與 m 可以是 0 到 256 的整數

                  如果要找的字串是 1 之後接著連續 2 到 4 個 0﹐再接著 1 時﹐我們可以把 RE 寫成 10/{2,4/}1﹐實驗一下:

                  [說明]

                  [問題 10]

                  [說明]

                  對一資料行執行多個編輯命令

                  到目前為止﹐我們在所舉的範例中﹐只對每一行執行一個編輯命令。事實上﹐sed 可以對每一行的內容執行多個命令:

                  sed -e 命令1 -e 命令2 .... -e 命令n 資料檔名

                  讀者也可以採用一行列出一個命令的方式﹐如

                  命令1
                  命令2
                  :
                  命令n

                  把這些命令依序集合在一個命令檔(script file)內﹐然後使用

                  sed -f 命令檔檔名 資料檔名

                  的方式來執行。

                  而 sed 處理資料的方式﹐是依序對資料檔的每一行執行以下步驟:

                  [問題 11]

                  [說明] [補充說明]

                  在下一期的內容中﹐我們將介紹延伸型的 RE﹑egrep﹑以及如何在 perl 中使用 RE。

                  參考文獻

                   


                本期介紹給讀者的內容有:

                • 延伸型 RE﹐包含
                  • +, ?, | 等特殊字元
                  • ( ) 的運用
                • 在 Perl 中使用 RE﹐包含
                  • 引號及引號型運算子
                  • m// 運算子與 =~ 的結合
                  • 特殊序列
                  • 引用合乎 RE 的字串之某些部分
                  • m// 的傳回值因執行環境而異
                  • m//g 的運用
                  • s/// 運算子

                延伸型 RE

                延伸型(extended)RE 提供了比基本型 RE 更多的特殊字元﹐如 +, ?, 以及 |:

                1. X+:
                  • 代表由 X, XX, XXX, ... 等這些由連續 N 個(N >=1)X 所構成 的 RE 的集合。當字串符合 X, XX, XXX,... 中其中任何一個 RE 時﹐ 就算符合 X+。
                  • 例如符合 ab+c 這個 RE 的字串有 abc, abbc, abbbc, ... 等﹐也就是字元 a 之後接著至少一個的 b﹐再接著 c;字串 ac 則不符合。
                  • 事實上﹐X+ 與 XX* 具有相同的意義(equivalent): 剛才提到的 ab+c 與 abb*c 是相等的; 我們也可以把 [0-9][0-9]* 寫成 [0-9]+。
                2. X?:
                  • 代表由空字串以及 X 這兩個 RE 所構成的集合。
                  • 空字串的長度為零﹐不含任何字元。
                  • 觀察 ab?c 這個 RE﹐由於符合 b? 的字串為空字串或 b﹐ 因此符合 ab?c 的字串只有 ac 與 abc﹐而 abbc, abbbc, ... 等則不符合。

                [問題12]
                請利用 egrep 把含有 long 或 loong 的資料行列出來。
                [說明]

                1. egrep 與 grep 的差別在於解譯 RE 的方式:前者採用延伸型﹐ 而後者採用基本型 RE 的語法。
                2. 比較 long 與 loong 時發現﹐這兩個字串只相差一個 'o'﹐ 可以把 RE 寫成 loo?ng﹐第二個 o 之後的 ? 代表這個 o 可有可無。
                3. 執行範例如下:
                  [changyj:~] cat data7
                  Edward Furloong
                  has
                  long hair.
                  
                  [changyj:~] egrep -n 'loo?ng' data7
                  1:Edward Furloong
                  3:long hair.
                  

                特殊字元『|』

                當利用『|』把幾個 RE 連接起來﹐例如 RE1 | RE2 | ... | REn 時﹐代表字串只要符合 RE1, RE2, ..., REn 中的任何一個 RE 時即可﹐讀者可以把上式解讀成 RE1 『或』RE2『或』... REn。『|』常被稱為 alternation operator。

                以下利用檔案 data8 來示範『|』的用法。首先看看檔案 data8 的內容:

                [changyj:~] cat data8
                I keep a cat and
                a dog. The cat
                likes to eat fish
                and the dog prefers
                roasted beef.
                

                想列出含有 dog『或』fish 字串的資料行時﹐可以把 RE 設計成『dog|fish』:

                [changyj:~] egrep -n 'dog|fish' data8
                2:a dog. The cat
                3:likes to eat fish
                4:and the dog prefers
                

                如果要列出含有 cat『或』beef 字串的資料行﹐使用『cat|beef』這個 RE 即可:

                [changyj:~] egrep -n 'cat|beef' data8
                1:I keep a cat and
                2:a dog. The cat
                5:roasted beef.
                

                而使用『cat|dog』這個 RE 則可列出含有 cat『或』dog 的資料行:

                [changyj:~] egrep -n 'cat|dog' data8
                1:I keep a cat and
                2:a dog. The cat
                4:and the dog prefers
                

                我們發現檔案 data8 的第二行同時含有 cat 與 dog。那麼 egrep 究竟是因為發現該行有 cat﹐還是該行有 dog 才列出該行?以下列出 egrep 處理該行的過程:

                1. 一開始的 A 點(請參考第一期的說明)在第一個字元﹐也就是字元 'a' 的位置。 egrep 先檢查在該位置是否可能出現符合 RE﹐也就是『cat|dog』的字串。 而處理『|』時的順序是由左而右﹐因此對於同一個 A 點﹐ egrep 會先檢查該位置是否出現 cat;否則則檢查該處是否有 dog 字串。 在發現該位置並未出現符合任何子 RE 的字串後﹐egrep 把 A 點移到下一個字元。
                2. 此時 A 點在第二個字元﹐由於該位置並未出現 cat 或 dog﹐ egrep 把 A 點移到下一個字元。
                3. 當 A 點在第三個字元時﹐雖然該處並沒有 cat 字串﹐但卻出現了 dog 字串符合子 RE『dog』﹐於是 egrep 把該行列出。
                4. 因此 egrep 是先發現該行有 dog 而把該行列出。

                『( )』的使用

                在延伸型 RE 中﹐『( )』有兩種功能﹐統稱為 Grouping:

                1. 維持『子 RE』的完整性:
                  當要設計只讓 company 及 companies 能符合的 RE 時﹐ 我們發現這兩個字串前半部都是 compan。請參閱表 1:
                  [表1]
                   第一部分 第二部分
                  合乎要求的字串 compan y 或 ies
                  相對應的 RE compan y | ies
                  如果把兩個部分的 RE 直接連接起來﹐得到『company|ies』﹐ 這個 RE 代表要搜尋的字串是 company 或是 ies﹐顯然與原先的構想不同。 解決的方法是藉由『( )』來維持『y|ies』的完整性﹐ 把 RE 寫為『compan(y|ies)』﹐執行範例如下:
                  [changyj:~] cat data9
                  Although there are many companies
                  selling computers, our
                  company is the most famous one.
                  
                  [changyj:~] egrep -n 'compan(y|ies)' data9
                  1:Although there are many companies
                  3:company is the most famous one.
                  
                2. 使多字元的 RE 與 *, +, ? 等特殊字元結合:
                  在介紹基本型 RE 時﹐曾強調 * 必須與佔有一個字元位置的 RE(one-character RE) 結合;在延伸型 RE 中﹐只要把多字元的 RE 利用一組『( )』含括起來 即可與 *, +, ? 等特殊字元結合。例如要尋找 cabc, cababc, cabababc, ... 等 由字元 c 再接著若干組 ab 再接上 c 的字串時﹐可以把 RE 設計為『c(ab)+c』﹐ 此時與特殊字元 + 結合的 RE 是 ab:
                  [changyj:~] echo 'ccababcc' | egrep -n 'c(ab)+c'
                  1:ccababcc
                  

                一般而言﹐grep, sed 等支援的是基本型 RE;而 egrep, perl 支援的是延伸型 RE。表 2 列出了延伸型 RE 的特殊字元﹐以及在基本型 RE 中相對應的寫法;若標示『無』則代表該類 RE 並未提供該項功能:

                [表2]
                延伸型 RE 的特殊字元 在基本型 RE 內的寫法
                '.', ^, $,
                [...], [^...]
                同左
                *, +, ?, |, {min,max} *,無,無,無,/{min,max/}
                (...) /(.../)
                (...)*, (...)+, (...)?
                [註1] 讀者在 GNU 的 grep 與 sed 中可以使用延伸型 RE 內的 + 與 ?﹐ 寫法分別是 /+ 與 /?。
                [註2] GNU 的 grep 與 sed 允許 *, /+, /? 與多字元的 RE 結合﹐ 只要把該 RE 放在一組 /( 及 /) 內即可。

                如何在 Perl 中使用 RE

                本節的目的在於介紹如何在 Perl 上使用 RE﹐筆者假設讀者對 Perl 已有基本的認識﹐文中將不詳述與 RE 無關的部分。

                引號與引號型的運算子

                在 Perl 中﹐我們必須在字串型資料的前後各加上一個單(雙)引號﹐例如 'It is fine...', "Are you OK?"﹐以便把不屬於該字串的其他字元分隔開來。

                Perl 對於單﹑雙引號有不同的處理方式:

                • 單引號:
                  把字串內的『//』視為『/』﹐『/'』視為『'』﹐其他則不受影響。
                • 雙引號:
                  1. 對 escape 序列進行處理:把 /t, /n, /r, /f 等分別視為 定位(TAB)字元﹑跳行(NL)字元﹑歸位(CR)以及跳頁(FF)字元等。
                  2. 若 $ 及 @ 前方沒有與其結合的 / 時﹐Perl 會把緊臨 $ 及 @ 之後的文數字元視為變數及陣列名稱﹐並把『$變數名』及『@陣列名』 換成變數及陣列的內容。

                請參考以下的範例:

                $var = '/n';   # $var 的內容為兩個字元:'/' 以及 'n'
                $var = "/n";   # $var 的內容為一個字元:跳行字元
                $name = 'John';
                $var = 'My name is $name..';  # $var 的內容為 My name is $name..
                $var = "My name is $name..";  # $var 的內容為 My name is John..
                

                相較於其他的程式語言﹐Perl 提供了『引號型運算子』﹐讓使用者可以選擇除了 ' 與 " 之外﹐任何非文數字(non-alphanumeric)及非空白字元(non-whitespace)的字元來做為分隔符號(delimiter):

                1. 單引號型運算子:
                  • 寫法為 q/字串/﹐與 '字串' 同義。例如 'It is fine...' 可寫成 q/It is fine.../(以 / 為分隔符號)﹐ 也可寫成 q!It is fine...!(以 ! 為分隔符號)等。
                  • 但如果採用 ()[]{}<>其中的字元來做為分隔符號時﹐ 則必須以 q(字串), q[字串], q{字串} 或 q<字串> 這種成對的格式來使用﹐例如:q(It is fine...)。
                2. 雙引號型運算子:寫法為 qq/字串/﹐與 "字串" 同義。

                有關引號型運算子的詳細說明﹐請參考 perlop manpage 內 'Quote and Quote-like Operators' 一節。

                尋找符合 RE 的字串-- =~ 與 m/RE/ 的結合

                要檢查一個變數是否含有符合 RE 的字串時﹐我們可以利用運算子 m/RE/ 或 /RE/﹐並與 =~ 合用﹐格式為 變數 =~ m/RE/ 或是 變數 =~ /RE/ 當變數含有符合 RE 的字串時﹐運算的結果為真﹐否則為偽。

                [問題13]
                請寫一 Perl 程式﹐當使用者輸入的資料含有數字時﹐印出『有數字』的訊息。
                [說明]
                程式如下:

                #!/usr/bin/perl
                while(1)
                 { print "請輸入: "; 
                   chop($input=<STDIN>);
                   if($input =~ m/[0-9]+/) { print "有數字.../n/n"; }
                 }
                

                在這個程式中:

                1. 有一個無窮迴圈(while(1) {...})。
                2. 使用者每輸入一行﹐ 該行的內容會被存入 $input 這個變數中($input=)﹐ 包含按 ENTER 時產生的跳行字元(newline﹐/n)。
                3. 我們利用 chop($input) 來刪除 $input 內的最後一個字元﹐也就是跳行字元。
                4. 當 $input 內含有數字時會使得 if(m/[0-9]+/) 成立而印出『有數字』的訊息。 例如輸入『number 235811 text 44759』時:
                  • m// 由變數內容前端開始搜尋。
                  • 首先發現 2 這個字元符合 [0-9]。
                  • 由於『+』是個貪心的字元﹐m// 會儘可能把從字元 2 開始的連續多個數字
                  • 字元納入 [0-9]+ 的範圍﹐因此 m// 找到第一個符合 /[0-9]+/ 的字串是 235811。

                採用 m/RE/ 而不是 /RE/ 的好處在於前者可以使用其他的字元來做為分隔符號﹐方式與先前介紹的『引號型運算子』相同。

                [問題14]
                請寫一 Perl 程式讓使用者輸入一路徑名稱﹐並判斷該路徑是否為絕對路徑。
                [說明]
                程式如下:

                #!/usr/bin/perl
                while(1)
                 { print "請輸入路徑: ";
                   chop($path=<STDIN>);
                   if($path =~ m|^/|) { print " $path 是絕對路徑/n/n"; }
                   else { print " $path 是相對路徑/n/n"; }
                 }
                

                在這個程式中﹐

                1. 我們以『 $path 的內容是否以 / 開頭』來做為是否為絕對路徑的依據。
                2. 其中 ^ 為定位字元﹐代表變數內容的最前端﹐ 如果為 $ 則為變數內容的末端。
                3. 如果不改變分隔符號的寫法是 $path =~ m/^///。
                4. 我們可以把 m|^/| 寫成 m(^/)﹑m[^/]﹑m{^/} 或 m<^/>。
                5. print 後的字串是以雙引號來做為分隔符號﹐而 '$path' 之前並沒有 /﹐ 因此 Perl 會以 $path 的內容來取代 '$path' 這五個字元。

                特殊序列

                為簡化 RE 的寫法﹐Perl 定義了一些特殊序列(meta sequence)﹐請參考表 3:

                [表三]
                特殊序列 說明
                /w 等於 [A-Za-z0-9_]﹐由文數字元(alphanumeric)及 底線 _ 所構成的字元集合
                /W 等於 [^A-Za-z0-9_], 為 /W 的反相字元集合
                /s 由空白字元所構成的字元集合﹐通常為 [ /t/n/r/f]
                /S /s 的反相字元集合
                /d 等於 [0-9]﹐由數字字元(digit)所構成的字元集合
                /D 等於 [^0-9]﹐為 /d 的反相字元集合

                因此﹐問題 13 中的 $input =~ m/[0-9]+/ 可改寫成 $input =~ m//d+/﹐也可寫成 $input =~ m{/d+} 等。

                引用合乎 RE 的字串之某些部分

                當變數內含有符合 RE 的字串時﹐Perl 允許我們引用該字串的某些部分﹐說明如下:

                1. 設計 RE 時在欲引用的部分前後各加上『(』與『)』做為標記﹐ 在此『( )』的功能是『擷取文字』(capturing text)。
                2. RE 中的『(』與『)』必須成對(不考慮與『/』結合者)。
                3. 把 RE 中的『(』加以編號﹐編號從 1 開始﹐由左向右遞增。
                4. 對於經由『( )』標記的部分﹐如果標記該組的『(』之編號為 N﹐ 則該組的編號亦為 N﹐我們利用『$N』來引用該組的內容。
                5. Perl 採用延伸型 RE﹐因此『( )』也能做為 grouping 之用 (請參考『( ) 的使用』一節)。 即使我們要某些『( )』擔任的是 grouping 的角色﹐Perl 仍賦予它擷取文字的 功能;因此在編號時仍須計算在內。
                6. 在 sed 中是利用『/(』與『/)』來標記﹐以『/編號』來取用﹐ 與 Perl 不同﹐請讀者留意。

                [問題15]
                如果 $var 內含有符合像 '12:23:52' 這種格式的字串時﹐請把該字串所代表的時﹑分﹑秒印出來﹐並分別存到 $hour, $min, $sec 內。
                [說明]

                1. 首先設計出讓 '12:23:52' 這種格式的字串能符合的 RE:『/d+:/d+:/d+』。 在此不使用 /d/d 而採用 /d+ 是為讓時(分﹑秒) 只有一位數的情況﹐像 8:15:4﹐也能符合所設計的 RE。
                2. 接著分別把代表時﹑分﹑秒的部分標記起來﹐得到『(/d+):(/d+):(/d+)』。 時﹑分﹑秒分別是第一﹑二﹑三組用『(』及『)』標記起來的部分。
                3. 當有字串符合『(/d+):(/d+):(/d+)』這個 RE 時﹐ 我們分別可用 $1, $2 及 $3 來引用字串中屬於時﹑分﹑秒的部分。 程式範例如下:
                  #!/usr/bin/perl
                  while(1)
                   { print "請輸入: "; 
                     chop($var=<STDIN>);
                     if($var =~ m/(/d+):(/d+):(/d+)/)
                      { $hour = $1; $min  = $2; $sec  = $3;
                        print " 時=$1, 分=$2, 秒=$3/n";
                        print " /$hour=$hour, /$min=$min, /$sec=$sec/n/n";
                      }
                   }
                  
                4. 如果想同時印出整個符合的字串時﹐可以把整個 RE 用一組『( )』 標記起來﹐成為『((/d)+:(/d+):(/d+))』。此時 $1 的值即為整個符合 的字串﹐而字串中屬於時﹑分﹑秒的部分則分別是 $2, $3 與 $4。程式改寫如下:
                  #!/usr/bin/perl
                  while(1)
                   { print "請輸入: "; 
                     chop($var=);
                     if($var =~ m/((/d+):(/d+):(/d+))/)
                      { $hour = $2; $min  = $3; $sec  = $4;
                        print " Time=$1, 時=$2, 分=$3, 秒=$4/n";
                        print " /$hour=$hour, /$min=$min, /$sec=$sec/n/n";
                      }
                   }
                  
                5. 由於 Time=13:246:7890 這種不合理的字串也符合剛才設計的 RE﹐ 為使程式更加完善﹐我們可以增加做為判斷之用的程式碼而改寫成
                  if($var =~ m/(/d+):(/d+):(/d+)/)
                   { if($1 >=24 || $2 >=60 || $3 >= 60)
                      { print "時間格式不正確!!/n"; 
                      }
                     else 
                      { $hour = $1; $min  = $2; $sec  = $3; 
                        print "/$hour=$hour, /$min=$min, /$sec=$sec/n"; 
                      }
                   }
                  
                  即可﹐不必堅持要改寫 RE;實際上在設計 RE 時﹐ 如果要把所有條件都考慮進去﹐往往是耗時且困難的。

                m// 的傳回值因執行環境而異

                在剛才的例子中﹐'if' 所能接受的值屬於純量型(scalar)﹐因此 'if' 要求 $var =~ m// 傳回純量值(例如真﹑偽);我們稱此時 $var =~ m// 是在純量環境(scalar context)下執行。

                如果要求 $var =~ m// 傳回串列(例如利用它的傳回值來設定陣列等)﹐則稱它是在串列環境(list context)下執行﹐分為兩種情形:

                1. RE 中有利用『(』及『)』標記起來的部分:
                  當變數含有符合 RE 的字串時﹐$var =~ m// 會傳回由 $1, $2, ... 等變數值 所構成的串列;否則傳回空的串列。

                  [問題16]
                  如果 $var 內含有符合像 '12:23:52' 這種格式的字串時﹐請印出該字串所記錄的時﹑分﹑秒。
                  [說明]
                  程式範例如下:

                  #!/usr/bin/perl
                  @label=('時','分','秒');
                  while(1)
                   { print "請輸入: ";
                     chop($var=<STDIN>);
                     $i=0;
                     foreach $x ($var =~ m/(/d+):(/d+):(/d+)/)
                      { print " $label[$i]=$x/n";
                        $i++;
                      }
                   } 
                  
                  (1) foreach $x 之後需要一個串列﹐因此 $var =~ m// 是在串列環境下執行。
                  (2) 如果使用者輸入『I'll meet him at 11:34:5 in the park.』時﹐ 由於 11:34:5 符合 RE﹐m// 將傳回由 ('11', '34', '5') 這樣的串列。 我們再透過 foreach 的迴圈把每一個元素列出。
                  (3) 如果使用者輸入『I hate regular expression!』時﹐由於其中並無符合 RE 的字串﹐m// 將傳回空串列;foreach 則不會執行迴圈。
                2. RE 中沒有利用『(』及『)』標記起來的部分: 當變數內有字串符合 RE 時﹐傳回串列 (1);否則傳回空的串列。

                m//g 的運用

                當 m// 找到第一個符合 RE 的字串後就不會再繼續搜尋;如果要求 m// 找出所有符合 RE 的字串時則必須使用 g 選項。在不同的執行環境下﹐m//g 也有不同的回應:

                1. 串列環境:
                  • RE 中有標記起來的部分時﹐m//g 傳回的串列是由 每一個符合 RE 的字串之 $1, $2, $3,... 所構成。 把問題 16 的程式改寫如下:
                    #!/usr/bin/perl
                    @label=('時','分','秒');
                    while(1)
                     { print "請輸入: ";
                       chop($var=<STDIN>);
                       $i=0;
                       foreach $x ($var =~ m/(/d+):(/d+):(/d+)/g)
                        { print " $label[$i]=$x  ";
                          $i++;
                          if($i == 3) { $i=0; print "/n"; }
                        }
                     } 
                    
                    此時如果輸入『meet him at 11:22:33, and her at 12:44:55』時﹐ foreach 所接收到的串列將是 ('11','22','33','12','44','55')。
                  • RE 中沒有標記起來的部分時﹐m//g 會傳回由所有符合 RE 的字串所構成的串列。 若把 $var=~ ... 改寫為『$var=~ m//d+:/d+:/d+/g』且輸入與剛才相同的資料時﹐ foreach 接收到的串列將是 ('11:22:33', '12:44:55')﹐ 與 m/(/d+:/d+:/d+)/g 有相同的效果。
                2. 純量環境:
                  m//g 會從上一個符合 RE 字串之後的第一個字元開始搜尋; 若是第一次搜尋或上次搜尋失敗時﹐則從變數內容的第一個字元開始。 當找到符合 RE 的字串時傳回值為真﹐否則為偽﹐ 通常配合迴圈來使用。以下的程式將列出 $input 內所有的數字:
                  #!/usr/bin/perl
                  while(1)
                   { print "請輸入: "; 
                     chop($input=<STDIN>);
                     print "找到的數字有 ";
                     $i=0;
                     while($input =~ m/(/d+)/g)
                      { $i++; print " $1 "; 
                      }
                     print "/n一共有 $i 個數字../n/n";
                   }
                  

                s/// 運算子

                還記得在 sed 中我們利用編輯命令 s 來進行字串取代的動作嗎? Perl 也提供了格式相同的 s 運算子﹐可用來修改變數的內容。範例如下:

                #!/usr/bin/perl
                $var = "The good boy has a good cat and a GooD dog.";
                print " /$var 先前的內容是 $var/n";
                $var =~ s/good/bad/;
                print " /$var 現在的內容是 $var/n";
                

                執行上述程式時發現在 $var 中只有『第一個』good 被換成 bad﹐如果要把所有的 good 換成 bad﹐則必須加上 g(代表 global 之意)選項﹐把敘述改寫成『$var =~ s/good/bad/g;』即可。如果不區分大小寫﹐要把 good, Good, gOOD 等都換為 bad 時﹐則必須再加上 i(代表 ignore case 之意)選項﹐把原敘述改為『$var =~ s/good/bad/gi;』即可。

                在 Perl 的 s/// 運算子中﹐不論是要引用符合 RE 的字串﹐或是字串的某些部分﹐或是改變分隔符號﹐均採用與 m// 相同的方式。做個實驗:

                #!/usr/bin/perl
                $var = "abc123def";
                print " /$var 先前的內容是 $var/n";
                $var =~ s/(/d)/$1$1/g;
                print " /$var 現在的內容是 $var/n";
                $path = "/usr/bin/ghostview";
                print " 原先 /$path 的內容是 $path/n";
                $path =~ s|/usr/bin|/usr/local/bin|;
                print " 現在 /$path 的內容是 $path/n";
                

                這個程式的前半部會把變數 $var 內每一個(因為用了 g 選項)數字字元重覆一次﹐因此 $var 的內容會變成 abc112233def。後半部則把 $path 內的 /usr/bin 換成 /usr/local/bin。

                [問題17]
                假設 $var 內容是由三個以『,』分隔開的欄位所構成﹐現在我們想把第 1 與第 2 個欄位對調。

                [說明]
                程式如下:

                #!/usr/bin/perl
                $var = "欄位 1, 欄位 2,  欄位 3";
                print " /$var 先前的內容是 $var/n";
                $var =~ s/^([^,]*),([^,]*)/$2,$1/; 
                print " /$var 現在的內容是 $var/n";
                

                由於是以逗號來區隔欄位﹐我們假設每個欄位內不會有逗點出現﹐因此可以把每個欄位的內容想成是由『零或多個非逗號的字元』 所構成﹐可以用『[^,]*』這個 RE 來描述。

                Perl 的 s/// 提供了一個很實用的選項 e﹐以例子來說明:

                #!/usr/bin/perl
                $var = "num 54321 num 1357 num 246";
                print " /$var 先前的內容是 $var/n";
                $var =~ s/(/d+)/sprintf("%06d", $1)/ge;
                print " /$var 現在的內容是 $var/n";
                
                1. s/// 的 RE 部分為 /d+﹐Perl 會嘗試以『貪心』(即越多越好) 的方式來尋找連續多個的數字字元。
                2. 在 $var 中﹐Perl 找到第一個符合 /d+ 的字串是 54321。 由於使用了 e 選項﹐Perl 會先把 sprint("%06d", $1) 當成一般的式子來執行﹐ 並以該式子所產生的資料來做為『用來取代的字串』。 而 sprintf("%06d", $1) 會把 $1 的值以六位數的方式印出﹐ 不足六位時會在前方以 0 補滿。 由於此時 $1 的值為 54321﹐sprintf() 的結果為 054321﹐因此 Perl 會把 $var 內的 54321 換成 054321。
                3. 第二﹑三個符合 /d+ 的字串分別是 1357 與 246﹐ 而執行 sprintf() 後產生的字串分別是 001357 與 000246﹐ 因此 Perl 分別用 001357 以及 000246 把兩者取代掉。

                結語

                筆者借用了三期的篇幅﹐向各位介紹 RE 的概念﹐並示範如何在 grep, sed, egrep 與 Perl 上使用 RE。三期的內容﹐只能讓各位初窺 RE 的奧妙﹐無法讓各位馬上成為 RE 的行家﹐畢竟要熟悉 RE﹐除了多加思考與練習﹐並沒有其他的捷徑。冀望將來能有機會﹐與各位一起更深入地研究正規表示式。

                參考書目

                • Perl 的 manpages:
                  Perl manpage 可算是第一手的參考資料﹐撰寫得相當詳細。 本期主要參考的部分為 perlre 及 perlop;前者介紹 Perl 中的 RE 機制﹐ 而後者著重於 perl 的運算子。由於本文所介紹只是 Perl RE 的一小部分﹐ 建議有心鑽研的讀者印出這兩份 manpage﹐詳細閱讀。
                • Mastering Regular Expressions﹐作者為 Jeffrey E. F. Friedl﹐ O'Reilly & Associates 出版﹐適合已熟悉 RE 的讀者進階之用。
                  • 第一章:簡介﹐以 egrep 來示範 RE 的用途。
                  • 第二章:簡介如何在 Perl 中使用 RE。
                  • 第三章:各種工具程式所提供的 RE 機制。
                  • 第四章:工具程式如何根據使用者指定的 RE 來進行搜尋。
                  • 第五章:如何建構一個有效率的好 RE。
                  • 第六章:有關 Awk, Tcl, GNU Emacs 在 RE 方面的資訊。
                  • 第七章:深入探討 Perl 提供的 RE 運算子。
                  很有份量及深度﹐不是一本容易讀的書﹐但讀者如果能好好讀過﹐ 相信將獲益良多。
                评论
                添加红包

                请填写红包祝福语或标题

                红包个数最小为10个

                红包金额最低5元

                当前余额3.43前往充值 >
                需支付:10.00
                成就一亿技术人!
                领取后你会自动成为博主和红包主的粉丝 规则
                hope_wisdom
                发出的红包
                实付
                使用余额支付
                点击重新获取
                扫码支付
                钱包余额 0

                抵扣说明:

                1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
                2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

                余额充值