命令行里的正则:grep -E、sed、awk 方言速查
先分清 BRE 与 ERE
Unix 文本三剑客的正则分两套古董方言:BRE(基本正则,grep 默认)里 + ? | ( ) { } 都要加反斜杠才有特殊含义;ERE(扩展正则,grep -E、awk 默认)里它们天生就是元字符。日常记住一条:一律用 -E 走 ERE,少跟反斜杠打架。
| 能力 | BRE(grep 默认) | ERE(grep -E) | 备注 |
|---|---|---|---|
| 分组 | \(...\) |
(...) |
写法相反 |
| 或(alternation) | 竖线前加反斜杠(GNU 扩展) | 直接写竖线 | 用 -E 免去大量转义 |
| 一次或多次 | x\+ |
x+ |
|
| 零或一次 | x\? |
x? |
|
| 区间次数 | x\{2,4\} |
x{2,4} |
还有一条最容易踩的坑:\d 在 grep/sed/awk 里不是数字。这些工具的 POSIX 方言里 \d 只是个普通的 d,数字老老实实写 [0-9]。
# 从订单日志里抓出 2024 年的失败请求行
grep -E '2024-[0-9]{2}-[0-9]{2}.*(FAIL|ERROR)' app.log
# 统计每种状态码出现的行数
grep -oE 'HTTP/1\.1" [0-9]{3}' access.log | sort | uniq -c
第一条里 (FAIL|ERROR) 的括号与竖线因为 -E 而无需转义;[0-9]{2} 替代了想当然的 \d{2}。
sed:流式替换的主力
sed 的核心是 s/模式/替换/标志,配合 -E 就是命令行里的"批量查找替换"。把日志里的美式日期 2024-10-26 改成 2024/10/26:
echo "2024-10-26 上线" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\2\/\3\/\1/'
# 输出:2024/10/26 上线
替换串里 \1 \2 引用分组;分隔符 / 若与内容冲突,可以换成 s|...|...| 或 s#...#...#——处理路径时省去转义爆炸。行内定位用地址区间:sed -E '/ERROR/s/[0-9]{4}/****/g' 只对含 ERROR 的行脱敏四位数字。
awk:字段思维 + 正则
awk 的世界观是"按列切",正则用来做行筛选与行内二次匹配。三种用法叠加:/模式/ 整行筛选、match() 配 substr 提取、gsub 行内替换:
# 打印状态码非 200 的行的第 1、9 列(IP 与状态码)
awk '$9 != 200 {print $1, $9}' access.log
# 用 match + RSTART 提取行里的第一个耗时数字
awk 'match($0, /cost=[0-9]+/) {print substr($0, RSTART+5, RLENGTH-5)}' app.log
match 会设置 RSTART(命中位置)与 RLENGTH(命中长度)两个内置变量,配合 substr 完成提取。列结构清晰时优先 $1 $9,正则只在"结构靠内容而非位置"时上——这是 awk 与纯正则工具的分工原则。
方言对照速查
| 需求 | grep -E | sed -E | awk |
|---|---|---|---|
| 只看匹配部分 | -o |
s/.*(...).*/\1/ |
match+substr |
| 多选一 | \| |
同左 | 同左 |
| 忽略大小写 | -i |
s/.../.../I(GNU) |
tolower() 预处理 |
| 数字 | [0-9](无 \d) |
[0-9] |
[0-9] |
| 统计匹配数 | -c |
- | 手动累加 |
避坑提醒:macOS 自带的 BSD 版 grep/sed 与 GNU 版参数有差异(BSD sed 的
-E大小写、-i必须带备份后缀),CI 机器与本地不一致时脚本会莫名挂掉。跨平台脚本里要么写明"需要 GNU coreutils",要么只用双方一致的最小公共集。另外 GNU 的grep -P虽然支持\d与环视,但 macOS 上没有,生产脚本别依赖 -P。
组合拳:一条管道的诞生
三剑客的真正威力在组合。任务:找出访问日志里的 5xx 来源 IP 并计数。假设 sample.log 有以下内容:
10.0.0.7 - "GET /a" 200
10.0.0.8 - "GET /b" 500
10.0.0.7 - "POST /c" 502
10.0.0.9 - "GET /d" 200
一条管道解决:
grep -E '" [0-9]{3}' sample.log | grep -E ' 5[0-9]{2}$' | awk '{print $1}' | sort | uniq -c
# 输出:
# 1 10.0.0.8
# 1 10.0.0.7
读法:第一个 grep 确认行里有状态码形状,第二个 grep 用结尾锚 5[0-9]{2}$ 筛出 5xx,awk 取第一列(IP),sort + uniq -c 完成计数。每一步都只做一件事,正则在管道里是"筛子"而不是"万能解析器"——这是 Unix 哲学与正则结合的精髓。
若要提取更细的字段,grep -oE 只输出命中部分:
grep -oE '" [0-9]{3}$' sample.log | sort | uniq -c
# 每种状态码出现一次计数:200 两次、500 一次、502 一次
把这两段在终端里跑一遍,观察每步输出如何"变形"——管道思维一旦建立,你处理日志的速度会有一个数量级的提升。
两条管道实战(可照抄)
实战一:全库批量替换的"预演-复核-落盘"三步。把代码库里的 OldClass 全部改名为 NewClass,直接上 sed -i 是赌命。标准流程:
# 第 1 步:预演——只列出将被改动的文件与行号,什么也不改
grep -rnE 'OldClass' src/
# 第 2 步:落盘——把命中的文件逐个交给 sed 原地替换
grep -rlE 'OldClass' src/ | xargs sed -i 's/OldClass/NewClass/g'
# 第 3 步:复核——确认零残留
grep -rnE 'OldClass' src/ # 无输出即通过
三个参数的分工:-r 递归目录,-l 只输出文件名(去重后正好像 xargs 的口粮),-n 显示行号供人工核对。第 2 步的 xargs 把上一段的标准输出逐个拼到 sed 命令尾部——grep 负责"哪些文件",sed 负责"怎么改",两把刀各司其职。想更稳一点,第 2 步先去掉 -i 跑一遍看 diff 形态的替换预览,确认无误再加回 -i。
实战二:统计代码库里重复定义的函数。排查"同一个函数名在多个文件里定义了几次":
grep -hoE 'def [a-z_]+' *.py | sed -E 's/def //' | sort | uniq -c | sort -rn
# 输出示例:
# 3 foo
# 1 bar
逐段读:-h 不输出文件名(只要内容)、-o 只输出命中片段,于是产出 def foo、def bar 的流水;sed 剪掉 def 前缀;sort 把同名聚拢(uniq -c 只认相邻重复,所以 sort 是它的前置必需品);uniq -c 计数;sort -rn 让重复最多的浮到最前。这条管道是"提取-清洗-聚合"的三段式标准型——把第一段的正则换成 import [a-z_]+ 就变成依赖统计,换成 TODO|FIXME 就变成技术债盘点。骨架不变,正则换弹。
从交互到脚本:把管道固化下来
在终端里调通的管道,值得固化成脚本而不是每次重敲。给上面的统计逻辑加个外壳:
#!/usr/bin/env bash
# 用法:./top5xx.sh <日志文件>
set -euo pipefail
f="$1"
grep -E '" [0-9]{3}' "$f" | grep -E ' 5[0-9]{2}$' | awk '{print $1}' | sort | uniq -c | sort -rn
三个工程细节让脚本靠得住:set -euo pipefail 让任何一环失败就立刻退出,避免"半个结果"误导人;文件名用参数传入而不是写死;末尾追加 sort -rn 让计数倒序,最严重的来源排最前。
另外两个日常高频的 grep 参数值得记入肌肉记忆:-v 反选(排除 DEBUG 行看问题行),-c 只输出计数(快速确认"改动前后命中数没变")。命令行的正则不求全,把十几个高频参数练到不用查手册,效率就上来了;低频参数留给 --help。
参考资源
- GNU grep 手册(正则语法章节):GNU Grep Manual
- GNU sed 手册:GNU Sed Manual
本章由 WorkBuddy 原创撰写,示例均经实际验证。
