正規表示式的入門與應用(一)
張耀仁, 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 四個字元之間的相對位置則是固定的, 下表中的『第幾個字元』所描述的就是這種相對位置。 我們先列出下表:
字模內的
第幾個字元
1 2 3 4
允許值 h e a r

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

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

grep '正規表示式' 檔名


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

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

字模內的
第幾個字元
1 2 3 4
允許值 b 或 r e a r

接著按『允許值』為每個位置建構 RE。其中, 第二, 三, 四個位置的寫法 與前例相同。而第一個位置上允許 b 或 p 兩個字元, 其相對應的 RE 是 [br], 其中 [ 及 ] 是特殊字元(metacharacter), 可用來描述字元集合(character class), 代表只有在這一對 [ ] 內的字元才是被允許的。 而[br] 也可以寫成 [rb], 因為符合這兩個 RE 的字元是相同的。 我們把上表再加上每個位置所對應的 RE 得到
字模內的
第幾個字元
1 2 3 4
允許值 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 結尾的字串呢? 跟先前一樣, 把需求列表如下:
字模內的
第幾個字元
1 2 3 4
允許值 除了 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 tree in
4:the rear garden since he

問題 1

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


做法

相信讀者對如何建構 RE 已經有了基本的概念, 因此可以輕鬆地列出下表:
字模內的
第幾個字元
1 2 3 4
允許值 < 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)字元可用, ^ 與 $:
我們做個實驗:
[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?

一行的第
幾個字元
行首 1234 5678
允許值 任意字元 the
對應的 RE ^ ..... the

把每個位置的 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 的方法類似上題, 列表如下:
字模內的
第幾個字元
123 4 5 6...
允許值 0X 或 x 0 到 9
A 到 F
a 到 f
間的任一字元
同左邊,
但可有可無
對應的 RE 0[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 indidoges 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' 雖不含任何 數字, 但仍被列出來的原因。

在下一期的內容中, 我們將更深入地介紹 sed 的功能, 延伸型的 RE, 以及 egrep。