shell03-grep 与正则表达式
grep 与正则表达式
目录
- grep 在文本三剑客中的位置
- 正则表达式:先搞懂这套语言
- 正则的字符类与转义
- 正则的量词与分组
- grep 基本用法与常用参数
- BRE 与 ERE:基本 vs 扩展正则
- grep 的退出码:脚本里的用法
- grep 实战:日志、配置、代码
- grep 家族:fgrep / egrep / rgrep
- 常见坑与技巧
- 总结与练习
- 参考资料
0. grep 在文本三剑客中的位置
Linux 运维每天都离不开处理文本:分析日志、检查配置、搜索代码。处理文本有三个主力工具,合称文本三剑客,分工明确:
- grep:筛行(找到匹配的行);
- sed:编辑(替换、增删行);
- awk:处理(按列提取、统计)。
grep 负责「从一堆文本里挑出我要的那些行」,是日志分析、配置检查、代码搜索的基础。它配合正则表达式使用——正则决定了「什么样的行算匹配」。
一句话:grep = 正则表达式 + 逐行匹配 + 输出匹配行。
一、正则表达式:先搞懂这套语言
1.1 什么是正则表达式
正则表达式(Regular Expression,简称正则/regex)是一套描述文本模式的「语言」。你不用一个个字符去精确匹配,而是用一套规则去描述「长什么样的文本算符合条件」。
举个例子,你想在日志里找出所有「包含 error 的行」,最简单的就是搜字面 error;但如果你想找出「以 ERROR 开头」或「以 error 结尾」的行,就得靠特殊符号来描述这种位置规律:
grep "error" app.log # 匹配包含 "error" 字样的行
grep "^ERROR" app.log # 匹配以 ERROR 开头的行
grep "error$" app.log # 匹配以 error 结尾的行
第 2、3 个例子里 ^ 和 $ 就是元字符——它们不是匹配某个字符本身,而是表示「行首」和「行尾」这种位置。这就是正则的核心思想:普通字符匹配自己,元字符描述规律。
1.2 两类字符:字面字符与元字符
| 类型 | 例子 | 含义 |
|---|---|---|
| 字面字符 | error、2024 | 匹配这些字符本身 |
| 元字符 | . * ^ $ [] | 有特殊含义 |
一个常见误区:正则里的「点」. 是个陷阱——它表示「任意一个字符」,而不是字面点号。后面讲转义时专门处理这个。
1.3 基础元字符速查
| 元字符 | 含义 | 例子 |
|---|---|---|
. |
任意单个字符 | a.c 匹配 abc、a1c |
^ |
行首 | ^root 以 root 开头 |
$ |
行尾 | root$ 以 root 结尾 |
* |
前一个字符重复 0 次或多次 | ab*c 匹配 ac、abc、abbc |
+ |
前一个字符重复 1 次或多次(ERE) | ab+c 匹配 abc、abbc |
? |
前一个字符出现 0 次或 1 次(ERE) | ab?c 匹配 ac、abc |
[] |
字符类,匹配其中一个 | [abc] 匹配 a 或 b 或 c |
[^] |
字符类取反 | [^0-9] 匹配非数字 |
| ` | ` | 或(ERE) |
() |
分组(ERE) | (ab)+ 匹配 ab、abab |
{} |
次数限定(ERE) | a{2,3} 匹配 aa、aaa |
\\ |
转义 | \. 匹配字面点号 |
这些元字符里,要特别理解两类:
.、^、$、*、[]是「基础」元字符,在基本正则(BRE)里就生效;+、?、|、()、{}这些在基本正则(BRE)里默认是字面字符,需要转义;在扩展正则(ERE)里才是元字符。详见第 5 章——这是初学者最容易懵的地方。
注意:
+、?、|、()、{}这些在基本正则(BRE)里默认是字面字符,需要转义;在扩展正则(ERE)里才是元字符。详见第 5 章。
二、正则的字符类与转义
2.1 字符类 [ ]
字符类 [] 表示「匹配方括号里的任意一个字符」。它把一批字符框起来,只要文本里有其中任意一个就算匹配:
grep "[abc]x" file # 匹配 ax、bx、cx
grep "[0-9]" file # 匹配任意数字
grep "[a-zA-Z]" file # 匹配任意大小写字母
[abc] 匹配 a、b、c 中任意一个;[0-9] 里的连字符 - 表示范围,等价于 0123456789 任意一个。
常用区间:
[0-9]:数字[a-z]、[A-Z]:字母[a-zA-Z0-9]:字母数字
2.2 取反 [^ ]
在字符类开头加 ^ 表示取反——匹配「不在括号里的任意字符」:
grep "[^0-9]" file # 匹配包含非数字字符的行
grep "[^a-z]" file # 匹配包含非小写字母的行
[^0-9] 是「任何一个非数字字符」。注意这里的 ^ 放在 [ 里面才是取反,和「行首」的 ^ 含义不同——位置决定含义。
2.3 预定义字符类
很多工具支持更友好的预定义字符类(POSIX 风格,写在 [[:...:]] 里):
| 写法 | 含义 |
|---|---|
[[:digit:]] |
数字 |
[[:alpha:]] |
字母 |
[[:alnum:]] |
字母和数字 |
[[:space:]] |
空白(空格、制表符等) |
[[:upper:]] / [[:lower:]] |
大写 / 小写 |
[[:punct:]] |
标点符号 |
grep "[[:digit:]]" file # 匹配含数字的行
grep "[[:space:]]" file # 匹配含空白的行
用
[[:digit:]]比[0-9]更清晰、不易出错。注意它的完整写法是[[:digit:]](两层方括号)——外层是字符类,内层是 POSIX 名称。
2.4 转义:匹配字面元字符
要匹配「字面」的元字符(比如真正的点号、星号、美元符号),用反斜杠转义:
grep "\\." file # 匹配包含字面点号 . 的行
grep "a\\*b" file # 匹配 a*b(字面星号)
grep "\$100" file # 匹配 $100(字面美元符号)
这里的原理是:点号本来是「任意字符」,\\. 让 grep 把它当成「字面点号」。在命令行双引号里,反斜杠要写两个(\\ 代表一个真实反斜杠)——因为 bash 的双引号会「吃掉」一层反斜杠,剩下的一层才传给 grep。这一点是命令行正则最容易出错的地方。
三、正则的量词与分组
3.1 量词:控制重复次数
量词用来修饰前一个字符或组的出现次数:
| 量词 | 含义 | 例子 |
|---|---|---|
* |
0 次或多次 | ab*c:ac、abc、abbc |
+ |
1 次或多次 | ab+c:abc、abbc |
? |
0 次或 1 次 | ab?c:ac、abc |
{n} |
恰好 n 次 | a{3}:aaa |
{n,} |
至少 n 次 | a{2,}:aa、aaa... |
{n,m} |
n 到 m 次 | a{2,3}:aa、aaa |
理解 * 是「0 次或多次」很关键:ab*c 中 b* 表示 b 可以出现 0 次(得到 ac)、1 次(abc)、多次(abbc),所以它能匹配 ac、abc、abbc 等。+ 则要求至少 1 次,所以 ab+c 不能匹配 ac。
{} 是更精确的次数控制。实战应用:
# ERE 用法(grep -E)
grep -E "colou?r" file # 匹配 color 或 colour
grep -E "[0-9]{4}-[0-9]{2}" file # 匹配形如 2024-01 的日期
grep -E "https?://" file # 匹配 http:// 或 https://
colou?r 里 u? 表示 u 出现 0 或 1 次,所以同时匹配 color 和 colour;https?:// 里 s? 表示 s 可有可无,同时匹配 http:// 和 https://。
+、?、{}需要grep -E(扩展正则)才作为元字符。这也是我们推荐用grep -E的原因。
3.2 分组 ( )
() 把一部分正则当成一个整体,可以对整组应用量词:
grep -E "(ab)+" file # 匹配 ab、abab、ababab...
grep -E "(error|warning):" file # 匹配 error: 或 warning:
(ab)+ 是「把 ab 当成一个整体,重复 1 次或多次」,所以能匹配 ab、abab、ababab。没有括号的话 ab+ 只让 b 重复,意思完全不同。
分组 + 量词 = 强大的组合能力,几乎所有的复杂模式都靠这个搭起来。
3.3 或 |
| 表示「或者」,在两个模式之间选择:
grep -E "cat|dog|fish" file # 匹配 cat、dog 或 fish
grep -E "^(ERROR|WARN)" file # 以 ERROR 或 WARN 开头
注意 cat|dog 的匹配范围:| 的优先级比较低,cat|dog 是「cat 或 dog」,而不是 cat 后面跟个 d...。如果你只想让 | 作用于某个局部,用括号包起来,比如 ^(ERROR|WARN)——这里括号保证了「以 ERROR 或 WARN 开头」整个作为一个单元。
四、grep 基本用法与常用参数
4.1 基本用法
grep 的基本语法是 grep 模式 文件,支持多种输入来源:
grep pattern file # 在文件里搜索
grep pattern file1 file2 # 在多个文件里搜索
grep pattern /dir/ # 在目录里的文件搜索(需 -r)
grep pattern - # 从标准输入搜索(管道)
- 在多个文件里搜索时,输出会在每行前面带上文件名,方便区分结果来自哪个文件;
- 搜索目录必须加
-r(递归),否则 grep 会报错(它默认不接受目录作为输入); grep pattern -或直接echo ... | grep pattern是从标准输入读,也就是常和管道配合:前一个命令的输出,用 grep 再筛一遍。
4.2 常用参数速查
| 参数 | 作用 |
|---|---|
-i |
忽略大小写 |
-v |
反选(输出不匹配的行) |
-n |
显示行号 |
-c |
统计匹配的行数 |
-l / -L |
只列出含或不含匹配的文件名 |
-r / -R |
递归搜索目录 |
-q |
静默,只返回退出码(不输出) |
-o |
只输出匹配的部分(不是整行) |
-w |
整词匹配 |
-E |
用扩展正则(ERE) |
-F |
固定字符串(不解释正则) |
-A n / -B n / -C n |
匹配行之后 / 之前 / 前后各 n 行 |
--color |
高亮匹配部分 |
这些参数是 grep 实战的「弹药库」,下面逐个演示最常见的几个。
4.3 逐个演示
grep -i "error" app.log # 忽略大小写匹配 error/Error/ERROR
grep -v "^#" nginx.conf # 排除注释行(不以 # 开头)
grep -n "listen" nginx.conf # 显示匹配行号
grep -c "error" app.log # 统计 error 出现次数
grep -r "root" /etc/ # 递归搜 /etc
grep -w "root" /etc/passwd # 整词匹配 root(不匹配 rooter)
grep -o "[0-9]\\+" file # 只输出数字部分
grep -C 2 "panic" kernel.log # 匹配行前后各 2 行
逐个解释几个容易混淆的:
-v:反选,输出不匹配的行。grep -v "^#"是「排除以 # 开头的行」,常用于去掉配置文件里的注释;-c:只输出匹配的行数(注意是行数不是次数,一行里多个匹配也只算 1);-w:整词匹配。grep -w root只匹配独立的root,不会误伤rooter、roots这类含 root 的词;-o:只输出匹配到的那一小段,而不是整行。grep -o "[0-9]\\+"会把一行里的数字段都单独抠出来,常用于提取 IP、时间戳等;-C 2:把匹配行的前后各 2 行也一起输出,看上下文用的(日志排查很常用)。
五、BRE 与 ERE:基本 vs 扩展正则
5.1 两种正则方言
你可能已经注意到,有的元字符要 grep -E 才生效。这是因为 GNU grep 支持两套正则语法:
- BRE(Basic Regular Expression,基本正则):grep 默认;
- ERE(Extended Regular Expression,扩展正则):
grep -E或egrep。
两套语法都能用,区别在于哪些字符被当成元字符。
5.2 核心区别
ERE 把更多字符当元字符,用起来更自然。对比同一个字符在两种语法里的待遇:
| 元字符 | BRE(默认) | ERE(-E) |
|---|---|---|
+ |
需转义 | 直接用 |
? |
需转义 | 直接用 |
| ` | ` | 需转义 |
() |
需转义 | 直接用 |
{} |
需转义 | 直接用 |
意思是:在 BRE 里 + 只是个普通字符(字面加号),要表示「重复 1 次或多次」得写 \+(转义成元字符);而在 ERE 里 + 直接就是元字符。看对比:
# BRE:+ 要转义
grep "ab\\+c" file
# ERE:+ 直接用
grep -E "ab+c" file
(顺便一提,GNU grep 的 BRE 里 \+、\?、\{\}\} 这些转义形式也被支持,但语法啰嗦。)
5.3 建议:写脚本用 grep -E
写正则时一律用 grep -E,因为 ERE 语法更直观、不用到处加反斜杠,可读性和正确性都更好。这是长期被验证的最佳实践。
早期
egrep命令现在已弃用,统一用grep -E。
六、grep 的退出码:脚本里的用法
grep 除了输出匹配的行,还返回一个退出码,这个退出码在脚本里价值极高——它让你能判断「到底有没有匹配到」。这正好接上第 1、2 篇讲的「命令退出码当条件」:
| 退出码 | 含义 |
|---|---|
| 0 | 有匹配 |
| 1 | 无匹配 |
| 2 | 出错(如文件不存在) |
- 0:匹配到了(成功);
- 1:没匹配到(这不是「错误」,只是「没有结果」);
- 2:出错(比如文件不存在、权限不够)——这是真正的异常。
脚本里最标准的用法,是 grep -q 配合退出码判断「某内容是否存在」:
if grep -q "error" /var/log/app.log; then
echo "日志里有 error"
else
echo "日志正常"
fi
-q 是静默模式:不输出任何匹配行,只返回退出码。所以 grep -q 非常适合「只判断在不在、不想看到一堆输出」的场景。
-q静默模式 + 退出码,是脚本判断「是否存在某内容」的标准做法,不会输出一堆匹配行。
七、grep 实战:日志、配置、代码
学完语法和参数,看几个真实场景怎么组合。
7.1 日志分析
# 统计 ERROR 出现次数
grep -c "ERROR" app.log
# 只看 5 开头的错误码
grep -E "ERROR|error.*5[0-9][0-9]" app.log
# 按时间戳匹配某时段日志
grep "2024-01-15 14:" app.log
第一个 -c 统计 ERROR 行数;第二个用 | 或 .* 组合出「错误码以 5 开头」的错误;第三个按时间前缀筛出某一天的日志。
7.2 配置检查
# 找出 nginx 配置里非注释、非空、含 server 的行
grep -vE "^\\s*#|^\\s*$" nginx.conf | grep "server"
# 检查配置里是否已开启某项
grep -q "worker_processes" nginx.conf && echo "已配置" || echo "未配置"
第一行 -vE 先排除注释行(^\s*#)和空行(^\s*$),再把结果管道给第二个 grep server,两步筛选出「非注释非空且含 server」的行。第二行用 grep -q 的退出码 + &&/||,一行实现「有则提示已配置,无则提示未配置」。
7.3 代码/文件搜索
# 递归搜索代码里所有使用某函数的文件
grep -rl "my_function" /path/to/project/
# 只列文件名
grep -l "TODO" src/*.py
-l 只列出文件名(而不是内容行),-r 递归。这两个组合(-rl)就是「找出所有用到了某函数的文件」,非常常用。
7.4 提取 IP 地址
grep -oE "([0-9]{1,3}\\.){3}[0-9]{1,3}" access.log | sort | uniq -c | sort -rn
这条命令拆开看:-oE 用扩展正则只提取匹配部分,正则 ([0-9]{1,3}\\.){3}[0-9]{1,3} 匹配「三段数字加一个点、再一段数字」的 IP 格式;然后 sort | uniq -c 排序去重并计数,最后 sort -rn 按次数降序。整条命令就是从日志里提取所有 IP、统计每个 IP 的出现次数、按次数降序排列——找出访问最多的 IP。
八、grep 家族:fgrep / egrep / rgrep
历史上 grep 还有几个「亲戚」命令,现在都已弃用,统一用带参数的 grep 即可:
- egrep:
grep -E的旧别名,已弃用; - fgrep:
grep -F的旧别名,把模式当固定字符串(不解释正则),搜索大量字面关键词时更快; - rgrep:
grep -r的旧别名。
其中 -F(fgrep 的现代写法)值得知道:当你搜的是不含任何正则元字符的普通文本时,用 -F 告诉 grep「别把这当正则、就当普通字符串搜」,既省去了转义麻烦、速度也更快:
# -F 固定字符串:搜索包含字面 "a.b*c" 的行(不用转义)
grep -F "a.b*c" file
# 等价旧写法
fgrep "a.b*c" file
如果不用 -F,a.b*c 会被当成正则(点匹配任意字符、星号是量词),结果完全不对。
统一用
grep -E和grep -F即可,egrep/fgrep/rgrep都是历史命令。
九、常见坑与技巧
9.1 点号是「任意字符」不是字面点
这是最经典的坑:. 在正则里是「任意一个字符」,不是字面点号。想匹配真正的点号必须转义:
grep "example.com" f.txt # 会匹配 exampleXcom(点匹配任意字符)!
grep "example\\.com" f.txt # 正确匹配 example.com
第一行 example.com 里的点会匹配任意字符,所以 exampleXcom、example-com 也会被匹配到;第二行 \\.(转义的点)才精确匹配字面 example.com。
9.2 变量里的特殊字符
如果你把模式放进变量,而变量里含正则元字符,直接引用会被当成正则。这时用 -F 把它当固定字符串:
keyword="a.b"
grep -F "$keyword" file # 按字面 a.b 搜
9.3 中文与 UTF-8
grep 默认按字节匹配,处理 UTF-8 中文时一般没问题,但 . 可能只匹配半个汉字(因为汉字是多个字节)。多数场景直接搜中文字面串即可,不要依赖 . 去匹配汉字。
9.4 性能
大文件用 -F(固定串)比正则快;只需判断是否存在用 -q 提前退出(匹配到第一个就停,不用读完整个文件)。
9.5 管道里 grep 无匹配会怎样
管道中 grep 无匹配返回 1。配合第 2 篇的 set -o pipefail,这会让整条管道失败。若你希望「无匹配不算错」,可在末尾加 || true 兜底。
十、总结与练习
10.1 核心速查
- grep 定位:筛行;
- 正则:点号表示任意字符、行首行尾、量词、字符类、或、分组;
- 参数:-i 忽略大小写、-v 反选、-n 行号、-c 计数、-r 递归、-q 静默、-o 只取匹配、-E 扩展正则、-F 固定串、-C 上下文;
- BRE vs ERE:ERE 更直观,用
grep -E; - 退出码:0 有匹配、1 无、2 出错,脚本里配 -q 判断。
10.2 动手练习
- 找出 /etc/passwd 里所有以 root 开头的行;
- 统计 nginx 日志里 404 状态出现次数;
- 用 -v 排除配置文件的注释和空行;
- 用 -E 匹配 IP 地址并去重统计;
- 写脚本用 grep -q 判断某关键字是否存在于文件。
参考资料
- GNU grep 手册(man grep):https://www.gnu.org/software/grep/manual/
- 菜鸟教程 - 正则表达式:https://www.runoob.com/regexp/regexp-tutorial.html
- 菜鸟教程 - grep 命令:https://www.runoob.com/linux/linux-comm-grep.html
