阶段四 · 管道与文本处理

grep:文本搜索利器

1. grep 是什么

grep(Global Regular Expression Print)是 Unix 下最核心的文本搜索工具.它的工作很简单:读取文本,逐行扫描,把包含指定模式的行打印出来.

grep 就像你拿一支荧光笔在读一本书--你说"把出现这个词的句子全给我标出来",grep 就一页页翻,找到一行画一行,只返回命中的行,其余全部忽略.

它通常出现在两个位置:

  1. 直接在文件上搜索
  2. 作为管道链条中的过滤器--上一级命令的输出经过 grep 筛掉不关心的行,只把相关数据传给下一级

grep 的家族

grep(使用 BRE 基本正则,? + { | ( ) 需要转义才能被识别为元字符) ·
egrep(等价于 grep -E,使用 ERE 扩展正则,特殊字符不用转义,更直观) ·
fgrep(等价于 grep -F,按字面文本匹配,不解释正则,速度快).

日常建议直接用 grep -E,正则语法更接近其他语言,少写一堆反斜杠.

2. 基本用法

2.1 语法与第一条 grep

$ grep pattern file
# ↑ ↑
# 找什么 在哪找

来一个真实的例子:

$ cat fruits.txt
apple
banana
Apple Pie
pineapple
grape

$ grep "apple" fruits.txt
apple
pineapple # ← 包含 "apple" 就算命中,"apple" 不需要是完整单词

两个值得注意的点:

  1. 默认大小写敏感--Apple 没被选中
  2. 默认部分匹配--pineapple 命中了,因为它包含 apple 这个子串

2.2 --color:高亮匹配部分

在输出中把匹配到的文本用颜色标出来,一目了然:

$ grep --color "apple" fruits.txt
apple # "apple" 部分会以红色高亮
pineapple # 其中的 "apple" 子串也会高亮

现代 Linux 发行版通常已经在 ~/.bashrc 中把 grep 别名成了 grep --color=auto(输出到终端时自动上色,输出到管道时不渲染颜色代码).

3. 常用选项

3.1 选项速查

选项 含义 示例
-i 忽略大小写(case-insensitive) grep -i "error" log.txt
-v 反转匹配--选包含模式的行(invert) grep -v "#" config.conf
-n 显示行号(line number) grep -n "TODO" *.py
-c 统计匹配行count grep -c "ERROR" app.log
-l 只列出list 匹配的文件名 grep -l "FIXME" src/**/*.py
-w word 匹配 grep -w "is" script.sh
-o only 只输出匹配的那部分文本 grep -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" access.log
-r / -R recursive 递归搜索目录 grep -r "import" src/
-E 启用Extended 扩展正则 `grep -E "error

3.2 重点选项详解

-i:忽略大小写

$ grep -i "apple" fruits.txt
apple
Apple Pie # ← 这次也命中了
pineapple

-n:显示行号

$ grep -n "import" main.py
3:import os
7:import json
15:from datetime import datetime
# 格式:行号:内容

行号让后续定位变得便捷--配合 vim / VS Code :行号 跳转,是最常用的组合之一.

-c:只统计数量

$ grep -c "ERROR" /var/log/app.log
42
# app.log 中有 42 行包含 "ERROR"

# 结合 -v 统计非注释行
$ grep -vc "^#" nginx.conf
18

4. 正则表达式

grep 名称中的 RE 就是 Regular Expression(正则表达式).理解了正则,grep 才真正从"搜索关键词"升级为"模式匹配引擎".

4.1 BRE vs ERE:grep 的两种正则方言

grep 默认使用 BRE(Basic Regular Expression),grep -E(或 egrep)使用 ERE(Extended Regular Expression).核心区别在于:BRE 中 ? + { } | ( ) 需要加反斜杠才能当元字符用,ERE 不需要.

模式                   BRE(默认 grep)          ERE(grep -E)
───────────────────── ────────────────── ──────────────────
匹配 a 或 b grep "a\|b" grep -E "a|b"
一个或多个数字 grep "[0-9]\+" grep -E "[0-9]+"
分组 + 量词 grep "\(ab\)\+" grep -E "(ab)+"
零或一个 grep "colou\?r" grep -E "colou?r"

建议日常统一用 grep -E--ERE 语法和 Python/JavaScript 的正则更接近,省得记两套转义规则.本节后续所有示例都使用 grep -E.

4.2 正则速查表

模式 含义 例子
. 任意一个字符(除换行) h.t 匹配 hot,hat,h3t
* 前一个字符出现 0 次或多次 ab*c 匹配 ac,abc,abbc
+ 前一个字符出现 1 次或多次 ab+c 匹配 abc,abbc,匹配 ac
? 前一个字符出现 0 次或 1 次 colou?r 匹配 color,colour
^ 行首 ^# 匹配以 # 开头的行(注释)
$ 行尾 ;$ 匹配以分号结尾的行
[abc] 字符集--a,b 或 c 中的任意一个 [Hh]ello 匹配 Hello,hello
[^abc] 否定字符集--不是 a,b,c 的任意字符 [^0-9] 匹配非数字
[a-z] 字符范围 [A-Z][a-z]+ 匹配首字母大写的单词
{n} 恰好 n 次 [0-9]{4} 匹配 4 位数字
{n,} 至少 n 次 [0-9]{3,} 匹配 3 位以上数字
{n,m} n 到 m 次 [a-z]{3,8} 匹配 3–8 个字母
` ` 或(alternation)
() 分组 + 捕获 `(error
\b 单词边界 \blog\b 只匹配 log,不匹配 blog

grep 正则 vs 编程语言正则

grep 的正则是面向的--^$ 表示首和尾,不是整个文本的开头和结尾.没有 \d(用 [0-9] 代替),没有 \w(用 [a-zA-Z0-9_] 代替),也没有非贪婪匹配(*?).这些是它与 Python/JS 正则在习惯上的最大差异.

4.3 实战模式

# 匹配 IP 地址(简化版)
$ grep -E "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" access.log

# 匹配有效的邮箱地址
$ grep -E "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}" contacts.txt

# 匹配所有非注释,非空行
$ grep -Ev "^#|^$" nginx.conf
# -E 扩展正则 + -v 反转 → "不要注释也不要空行"

# 匹配以 "2024-01" 开头的日志行
$ grep -E "^2024-01" app.log

# 匹配包含 "ERROR" 但不包含 "ignore" 的行(需要两根管道)
$ grep "ERROR" app.log | grep -v "ignore"

5. 上下文控制:-A -B -C

搜索日志时,光看到匹配行往往不够--需要知道前后发生了什么.grep 提供了三个选项来控制上下文行数:

选项 含义 记忆
-A n 显示匹配行及其 n 行 After
-B n 显示匹配行及其 n 行 Before
-C n 显示匹配行及其前后各 n 行 Context
$ cat app.log
2024-06-01 10:00:01 INFO Server started
2024-06-01 10:00:05 DEBUG Loading config
2024-06-01 10:00:10 ERROR Connection refused: database unreachable
2024-06-01 10:00:11 WARN Retrying in 5s...
2024-06-01 10:00:16 ERROR Connection refused: database unreachable
2024-06-01 10:00:20 FATAL Max retries exceeded, shutting down

# 找到每个 ERROR 以及它的前 1 行和后 2 行
$ grep -B 1 -A 2 "ERROR" app.log
2024-06-01 10:00:05 DEBUG Loading config
2024-06-01 10:00:10 ERROR Connection refused: database unreachable
2024-06-01 10:00:11 WARN Retrying in 5s...
2024-06-01 10:00:16 ERROR Connection refused: database unreachable
--
2024-06-01 10:00:11 WARN Retrying in 5s...
2024-06-01 10:00:16 ERROR Connection refused: database unreachable
2024-06-01 10:00:20 FATAL Max retries exceeded, shutting down

# -C 3 等价于 -B 3 -A 3
$ grep -C 3 "ERROR" app.log

注意不同匹配块之间的 -- 分隔符.这是 grep 帮你加的,防止上下文混在一起.

6. 递归搜索:-r 与 -R

-r(或 -R)让 grep 递归进入目录,搜索其中的所有文件.两者的区别仅在于符号链接的处理:-R 会跟随符号链接,-r 不会--绝大多数场景用 -r 即可.

# 在当前项目的所有 Python 文件中搜索 "import"
$ grep -r "import" src/

# 输出带文件名--每个匹配行的前面标明来源
src/main.py:import os
src/main.py:import json
src/utils.py:import re
src/utils.py:from datetime import datetime

6.1 --include / --exclude:过滤搜索范围

递归搜索通常会匹配到不需要的文件(二进制文件,node_modules,.git 目录等).--include--exclude 支持 glob 通配符来限定范围:

# 只在 .py 文件中搜索
$ grep -r --include="*.py" "import" src/

# 排除 node_modules 和 .git
$ grep -r --exclude-dir={node_modules,.git} "TODO" .

# 只搜 .js 和 .ts,同时排除 dist 目录
$ grep -r --include="*.js" --include="*.ts" --exclude-dir=dist "useState" src/

# 排除二进制文件(PDF,图片等)
$ grep -r --exclude="*.{pdf,png,jpg,zip}" "password" .

--exclude-dir 排除的是目录名而不是路径.比如 --exclude-dir=build 会排除所有名为 build 的目录(无论嵌套多深).

7. 管道中的 grep

grep 的最大价值不在独立使用,而是作为管道链条中的筛选关卡.回顾第 10 篇--管道把上一个命令的 stdout 传给下一个命令的 stdin,grep 恰好是 stdin 的天然消费者.

7.1 过滤模式:筛出感兴趣的行

# 进程列表中找到所有 node 进程
$ ps aux | grep "node"

# 大目录下找配置文件
$ ls -la /etc | grep "\.conf"

# 查看 443 端口的监听情况
$ ss -tlnp | grep ":443"

# 只看 Docker 容器的运行状态
$ docker ps | grep "Up"

7.2 统计模式:数一数有多少

# 统计错误日志行数
$ grep -c "ERROR" /var/log/app.log

# 统计正在运行的容器数量
$ docker ps | grep -c "Up"

# 统计某个目录下的 JS 文件数
$ ls | grep -c "\.js$"

# 看有多少个用户有 shell 登录权限
$ grep -c "/bin/bash" /etc/passwd

7.3 多重 grep 串联:逐步收紧条件

多个 grep 串联等于AND 逻辑--每一级筛掉一批行,最后剩下同时满足所有条件的:

# 找出包含 "import" 但不含 "test" 的行
$ grep "import" src/main.py | grep -v "test"

# 找出 500 错误但不是超时的行
$ grep " 500 " access.log | grep -v "timeout"

# 看所有包含 TODO 但不在注释行里的
$ grep -r "TODO" src/ | grep -v "^#" | grep -v "^//"

# 三连过滤:ERROR 日志 → 只看昨天 → 排除已知的无害错误
$ grep "ERROR" app.log \
| grep "^2024-06-16" \
| grep -v "known-false-positive"

8. 进阶技巧

8.1 反向匹配:-v

-v 是运维的日常--"把不关心的行排除掉"比"把关心的行挑出来"更常用:

# 看配置文件时忽略注释
$ grep -v "^#" nginx.conf

# 同时忽略注释和空行
$ grep -v "^#" nginx.conf | grep -v "^$"
# 或一行搞定
$ grep -Ev "^#|^$" nginx.conf

# 排除多个不关心的进程
$ ps aux | grep -v "grep" | grep -v "ssh"

为什么 ps | grep ... 总要多 grep -v grep?

ps aux | grep "nginx" 的结果里总会包含 grep 进程自己:grep nginx 这行也含 "nginx".加一个 | grep -v "grep" 把它过滤掉,或者用正则技巧:ps aux | grep "[n]ginx"--[n] 匹配 n,但 grep 进程的命令行是 [n]ginx(方括号),这个模式不匹配自己,于是巧妙地避开了自引用.

8.2 精确词匹配:-w

-w 让 grep 只匹配完整的单词(被非单词字符包围):

$ cat words.txt
is
this
his
history

$ grep "is" words.txt
is
this
his
history

$ grep -w "is" words.txt
is

-w 等价于在模式两边各加一个 \b(单词边界),不需要手动写了.

8.3 只列文件名:-l

递归出几百个匹配时,往往只需要知道"哪些文件里有":

# 哪些源文件里引用了这个函数
$ grep -rl "oldFunction" src/

# 结合 -L 反过来--哪些文件里没有测试
$ grep -rL "import unittest" src/

8.4 只输出匹配部分:-o

配合正则,-o 能从行中抠出真正想要的数据:

# 从日志中提取所有 IP 地址
$ grep -Eo "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" access.log | sort | uniq -c | sort -rn
423 10.0.1.5
156 10.0.2.8
89 192.168.1.100

# 从 HTML 中提取所有链接
$ grep -Eo 'href="[^"]*"' index.html

# 从 git log 中提取所有 commit hash
$ git log | grep -Eo "[0-9a-f]{40}" | head

快速回顾

  • grep = 逐行扫描 + 模式匹配:读 stdin 或文件,按行检查,把包含模式的行输出
  • 核心选项:-i 忽略大小写 · -v 反转 · -n 行号 · -c 计数 · -r 递归 · -E 扩展正则
  • ERE 更友好:grep -E 的正则语法比默认 BRE 更接近 Python/JS--+ | () 不需要反斜杠
  • 上下文控制:-A(After) / -B(Before) / -C(Context) 控制上下文行数,看日志排错时必用
  • 递归搜索:grep -r "pattern" dir/,配合 --include / --exclude 限定文件范围
  • 管道过滤:cmd | grep "条件" | cmd2--多级串联等于 AND 逻辑
  • 精准操作:-w 精确匹配单词 · -l 只列文件名 · -o 只抠匹配部分
  • 排除自引用:grep 常出现在 ps 结果中,用 grep -v grepgrep "[p]attern" 排除自己

动手练习

  1. 大小写匹配:创建一个测试文件,混入大小写不同的同一单词,分别用 grepgrep -i 搜索,体会区别
  2. ERE 或运算:用 grep -E "error|warning|fatal" 搜索某个日志文件(或 /var/log/system.log),比较与三次普通 grep 的效率差异
  3. 递归搜索:找一个项目目录,用 grep -rn "TODO" . 找出所有待办注释,尝试加上 --include 限定文件类型
  4. 上下文查看:用 grep -B 2 -A 3 "ERROR" 查看错误日志的上下文,观察 -- 分隔符的行为
  5. 排除自引用:运行 ps aux | grep "[t]erminal" 看看是否避免了 grep 自引用,再试试去掉方括号观察差异
  6. 提取匹配:用 grep -o 从一段文本中提取所有符合某种模式的数据(如 grep -Eo "[0-9]+" file 提取所有数字)
  7. 统计行数:用 grep -c 统计一个项目目录下所有源文件的代码行数中,有多少行是 import 语句