Learning Objectives
- 用 grep 找出含有關鍵字的行,搭配 -i、-n、-v、-c、-r 選項
- 用管線把其他指令的輸出交給 grep,再交給 wc 計算符合的行數
- 用 ^ 與 $ 比對行首與行尾,用 . 與 * 比對任意字元與重複
- 用 [abc]、[^a-z]、[[:digit:]] 等集合比對一個字元,知道基本正規表達式裡 + ? | 要加反斜線
用樣式描述想找的文字
設定檔與紀錄檔動輒上千行,要找「含有 error 的行」「以 # 開頭的註解」「有數字的行」,一行一行看並不實際。
正規表達式(regular expression,簡稱 regex 或 RE)是一套描述文字樣式的寫法,grep、sed、awk、各種程式語言與編輯器都支援。
它以「行」為單位處理文字,可以搭配特殊符號完成搜尋、刪除、取代。
grep 會逐行檢查輸入,印出符合樣式的整行。名稱來自早期編輯器 ed 的指令 g/re/p(global/regular expression/print)。
注意:正規表達式和上一課的萬用字元長得很像,意思卻不同。
符號 萬用字元(shell 比對檔名) 正規表達式(grep 比對文字內容) *任意多個字元 前一個字元重複 0 次以上 ?剛好 1 個字元 前一個字元可有可無(基本正規表達式要寫 \?).一般的點 任意 1 個字元 所以樣式一定要用單引號包起來,避免 shell 把它當成萬用字元先展開。
grep 的常用選項
grep [選項] '樣式' 檔案…
指令 | grep [選項] '樣式'
| 選項 | 作用 |
|---|---|
-i |
不分大小寫(ignore case) |
-n |
在每一行前面加上行號 |
-v |
反向:列出不符合的行(invert) |
-c |
只顯示符合的行數(count) |
-r |
遞迴搜尋目錄底下所有檔案(recursive) |
-l |
只列出含有符合內容的檔名 |
-o |
只印出符合的那一段文字,而不是整行 |
以 /etc/os-release 為例:
$ grep ubuntu /etc/os-release
ID=ubuntu
HOME_URL="https://www.ubuntu.com/"
$ grep -in ubuntu /etc/os-release
1:PRETTY_NAME="Ubuntu 24.04.1 LTS"
2:NAME="Ubuntu"
6:ID=ubuntu
8:HOME_URL="https://www.ubuntu.com/"
9:UBUNTU_CODENAME=noble
$ grep -c ubuntu /etc/os-release
2
不加 -i 時大小寫視為不同,所以 Ubuntu、UBUNTU 都沒有被找到。
本課的輸出取自練習用的模擬器,模擬器裡的
/etc/os-release與/etc/passwd是精簡過的版本; 真的 Ubuntu 這兩個檔案行數較多,找到的行與計數會比這裡多。
grep 也能放在管線裡,篩選其他指令的輸出;再接 wc -l 就能數出有幾行:
$ cat /etc/passwd | grep root
root:x:0:0:root:/root:/bin/bash
$ grep nologin /etc/passwd | wc -l
4
grep -r 樣式 目錄 會搜尋目錄底下所有檔案,每一行前面加上檔名。一般使用者讀不到的檔案會出現 Permission denied,
和上一課的 find 一樣可以用 2>/dev/null 丟掉。投影片的 grep network /etc/*.conf | wc -l 則是用萬用字元列出多個檔案,再數出符合的行數。
正規表達式的符號
| 符號 | 意思 | 例子 | 符合 |
|---|---|---|---|
^word |
word 在行首 |
^# |
以 # 開頭的註解行 |
word$ |
word 在行尾 |
驚嘆號後面接行尾符號 | 以 ! 結尾的行 |
^$ |
行首緊接著行尾 | (同左) | 空白行 |
. |
任意 1 個字元 | c.t |
cat、cut(不含 ct) |
* |
前一個字元重複 0 次以上 | ca*t |
ct、cat、caaat |
[abc] |
括號內任一個字元 | c[au]t |
cat、cut |
[a-z]、[0-9] |
範圍內任一個字元 | [0-9] |
含有數字的行 |
[^a-z] |
不在括號內的任一個字元(^ 寫在括號內的第一個) |
^[^#] |
不是以 # 開頭的行 |
[[:digit:]] |
字元類別:數字;還有 [:upper:]、[:lower:]、[:alpha:]、[:space:] |
[[:upper:]] |
含有大寫字母的行 |
\{n,m\} |
前一個字元連續 n 到 m 次(\{n\} 剛好 n 次、\{n,\} n 次以上) |
a\{2,\} |
caaat |
投影片的行首行尾範例:
grep -n '^#' regular_express.txt # 以 # 開頭的行,並列出行號
grep -n '!$' regular_express.txt # 以 ! 結尾的行
grep -n '^$' regular_express.txt # 空白行
幾個容易混淆的地方:
^寫在中括號外面是行首,寫在中括號裡的第一個是「不在範圍內」。*不是「任意字元」,而是前一個字元的次數,所以ca*t連ct都符合;要表示「任意多個任意字元」寫.*。[[:digit:]]外面那層中括號是集合,裡面的[:digit:]是類別名稱,兩層都要寫。字元類別會依語系設定判斷, 寫起來比[0-9]、[a-z]長,但意思比較明確。
基本與延伸正規表達式
grep 預設使用基本正規表達式(BRE)。在 BRE 裡,+、?、|、(、)、{、} 都是一般字元,
要在前面加反斜線才有特殊意義(GNU grep 的寫法):
| 意思 | 基本(grep) |
延伸(grep -E,下一課) |
|---|---|---|
| 前一個字元 1 次以上 | ca\+t |
ca+t |
| 前一個字元 0 或 1 次 | colou\?r |
colou?r |
「或」也一樣:基本正規表達式寫成 cat\|dog,延伸正規表達式直接寫 cat|dog。
所以 grep 'ca+t' 找的是「c、a、加號、t」這四個字元,通常什麼都找不到。投影片的 grep "ab\|cd" 就是 BRE 的「或」。
下一課的 grep -E(舊寫法 egrep)使用延伸正規表達式,這些符號就不必加反斜線。
想一想:grep 的選項
要列出 /etc/passwd 裡不含 nologin 的行,並顯示行號,應該用哪一個?
grep -c error app.log 與 grep error app.log | wc -l 的結果有什麼關係?
要在 /etc 底下所有檔案裡找 PermitRootLogin,而且不想看到 Permission denied,哪一個寫法正確?
想一想:正規表達式的符號
grep '^[^#]' sshd_config 會列出哪些行?
下列哪一個字串不符合正規表達式 go*d?
在 grep(沒有加 -E)裡想找 ab 或 cd,哪一個寫法正確?
重點精華回顧
grep '樣式' 檔案印出符合的整行;-i不分大小寫、-n行號、-v反向、-c計數、-r遞迴搜尋目錄、-l只列檔名。grep可以放在管線中篩選其他指令的輸出,再接wc -l計算行數;2>/dev/null丟掉 Permission denied。- 樣式用單引號包起來;正規表達式的
*、.、?和萬用字元意思不同。 ^是行首,$是行尾;兩個連在一起就是空白行。.任意一個字元、*前一個字元 0 次以上、.*任意多個任意字元。[abc]其中一個、[a-z]範圍、[^a-z]不在範圍內、[[:digit:]]字元類別、\{n,m\}重複次數。- 基本正規表達式(
grep)的+、?、|要寫成\+、\?、\|;下一課的grep -E不必加反斜線。