命令行里的正则: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。

参考资源

本章由 WorkBuddy 原创撰写,示例均经实际验证。