Linux三剑客grep/sed/awk全解析:管道原理到日志分析实战 凌晨两点某服务告警我登录跳板机面对几万行日志。一个终端窗口、一条管道命令grep、sed、awk 轮番上阵先过滤出 ERROR 关键字再把时间戳和堆栈噪声剥掉最后聚成一张统计表。整个过程三五分钟旁边同事还在 GUI 工具里导日志。这套三剑客组合大概是运维日常里最高频、也最被低估的三样东西。这次我把 sed、awk、grep 的底层逻辑和实战经验一次性整理出来。不是背诵参数手册而是从为什么会这样的角度把流式处理、正则差异、字段模型这些底层机制讲清楚。适合刚入行的运维、偶尔要开日志的开发者也适合已经会用但经常被引号转义或正则差异整懵的老手。读完你至少能带走一套完整的命令行文本处理体系而不是零散的命令片段。1. 三个命令的分工为什么文本处理偏偏是这三个家伙1.1 一条日志在管道里的旅程我先说一个最常见的场景。现在有一条应用日志里面混着 INFO、WARN、ERROR还有方括号包着的时间戳。你想知道每种日志级别分别出现在哪些模块最顺手的命令是grep -E ERROR|WARN app.log \ | sed s/^\[[^]]*\] // \ | awk {cnt[$1]} END {for (k in cnt) print k, cnt[k]}这条命令里三个工具各干各的活。grep 负责把包含 ERROR 或 WARN 的行从几万行里捞出来相当于给数据做第一次粗筛sed 拿到这些行之后把开头的[2025-02-05 14:03:01]这种时间戳整段删掉露出真正的日志内容awk 再按空白把剩下的内容拆成字段取第一个字段当成模块名做计数。这就是它们最自然的分工grep 管筛sed 管改awk 管算。三者不是同一个工具的三种实现而是流水线上的不同工位。数据以流的方式从上一个工具的输出进入下一个工具的输入每个工具只做自己最擅长的一件事然后交给下家。1.2 选型逻辑先想清楚你要筛、改还是算很多人学这三个命令的时候总喜欢问grep 能不能统计awk 能不能替换sed 能不能过滤——当然都能但能不能和该不该是两回事。我给自己定的选型逻辑是这样一张表任务类型首选工具理由只想知道有没有匹配行、匹配了几行grep最轻量不做多余的事纯流式扫描把命中行里的某一段文本替换掉、删掉sed它是专门的流式文本编辑器改字符串是看家本事按列拆分、聚合统计、条件计算awk有字段概念和内置变量天然适合做表格类计算排序、去重、取前 N 行sort / uniq / head配套工具经常出现在管道后半段解析 JSON / YAML 等结构化数据jq / yq三剑客只认文本不要拿正则去硬刚嵌套结构记住这套分工之后大部分场景你都不用犹豫先想这个任务属于筛、改还是算再决定用哪个复杂任务就按顺序串起来。grep 把数据切小sed 把格式弄干净awk 最后算结果这是最常见的黄金管道。1.3 什么时候根本用不着三剑客有些场景别被三剑客这个词绑架。比如你就想看一眼文件前几行head -5就够了想一边翻日志一边往后追用tail -f文件量大到几 GB 且要反复做多层关联计算硬写 awk 也能跑但维护成本很高不如交给脚本语言。还有一类是结构化数据。日志里偶尔会混 JSON 片段你用 sed 和 awk 去抠里面的某个 key往往要写很长的正则还容易漏掉嵌套层级。这种问题用专门的解析器干净得多。三剑客擅长的是格式不太规整的半结构化文本遇到真正规整的结构化数据换工具不是认怂是效率问题。2. grep表面是搜文件实则是整条流水线的守门员2.1 它为什么快逐行流式 正则引擎grep 的核心工作模型很简单逐行读入逐行匹配命中就输出不命中就丢弃。它不会先把整个文件装进内存再处理所以文件 100MB 还是 10GB内存占用基本稳定。这一点解释了很多运维敢在 GB 级日志文件上直接跑 grep 的底气。但逐行读入只是大框架。grep 真正快的原因在于它对不同模式做了不同优化。遇到grep error file这样的纯字符串匹配底层会走字节匹配类算法跳过大量不可能命中的区域遇到复杂正则则会被编译成状态机来处理。相比之下如果你用 awk 做同样的事awk 还要额外承担字段拆分的开销。所以单纯过滤grep 永远是第一选择。grep 的正则默认是基本正则BRE这是很多新手踩坑的根源。在 BRE 模式下|、、?、(、)这些字符默认都是普通字符想表达或、号、可选都得在前面加反斜杠。比如# 错误示范在 BRE 模式下匹配 ERROR 或 FATAL grep ERROR|FATAL app.log # 这是把 | 当成普通字符匹配ERROR|FATAL字面量 # 正确做法 1转义 grep ERROR\|FATAL app.log # 正确做法 2用 -E 切换到扩展正则 grep -E ERROR|FATAL app.log我强烈建议凡是正则稍微复杂一点就直接用-E也就是 ERE扩展正则模式。它接近你在大多数语言里写的正则习惯少一层反斜杠读起来也舒服。2.2 高频参数每个都是真实场景里摸出来的grep 的参数看起来多真正高频的就那么十几个。我按使用频率说# 忽略大小写排查告警时经常要同时匹配 Error / ERROR / error grep -i error app.log # 反向匹配看除了注释行之外的有效配置 grep -v ^# /etc/某服务的配置文件.conf # 计数配合 CI 脚本做断言时非常有用 grep -c ERROR app.log # 显示行号排查长日志时没有行号等于大海捞针 grep -n ERROR app.log # 只输出匹配到的部分而不是整行常用于配合后续统计 echo 2025-02-05 14:03:22 [ERROR] DB timeout | grep -o [A-Z]* # 输出ERROR # 递归搜索目录搜配置目录时最常用 grep -rn --include*.conf listen /etc/ # 带上下文输出查错误时最关键的是看错误前后发生了什么 grep -A 3 -B 1 FATAL app.log # -A 3 表示命中行后面 3 行-B 1 表示命中行前面 1 行-o是我特别想强调的一个参数。它把 grep 从整行筛选器变成字段提取器。比如你想从日志里抠出所有 IP可以写grep -oE [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} app.log配合后面的 sort、awk 就能直接做频次统计。这个思路比单纯grep -c灵活得多因为你拿到的是原始数据而不是一个被汇总过的数字。2.3 实战从几万行日志里扒出错误链路真实排查中我很少只用一个 grep。最常见的组合是这样的grep -nE ERROR|FATAL|Exception -A 2 -B 1 app.log | head -80意思是把包含 ERROR、FATAL、Exception 的行全部列出来并且每个命中行带着前 1 行、后 2 行的上下文加上行号先看前 80 行。这样做的好处是你不需要打开一个几十 MB 的日志文件去手动翻一秒之内就能定位到异常附近的代码路径。如果日志里错误一天有几千条我会再加一个时间维度的过滤。比如只查下午 14 点到 15 点之间的错误grep -E ERROR|FATAL app.log | grep 14:这里存在一个小技巧第二次 grep 再用grep 14:而不是把它们合并进第一个正则。分开写的好处是逻辑清晰后面想加别的条件直接再接一个管道即可。这比写一个长到看不清的复合正则更容易维护。2.4 容易翻车的细节正则、颜色、二进制用 grep 这几年我在线上翻过几次车说几个典型。首先是正则的点号陷阱。日志里常见ERROR in com.example.service.UserService.execute这种带包名和类名的内容。如果你写grep com.example在正则语义里每个点号都表示任意一个字符它确实能匹配到com.example但同时也会匹配comXexample这类你根本不想看到的内容。想要明确匹配点号本身必须写成com\.example。其次是管道里的颜色问题。grep 默认会在终端里给命中内容加颜色这个在终端里是好事但一旦管道交给 sed 或 awk颜色转义码会被当成文本传给下游导致字段解析错乱。所以你只要看到明明匹配了但 awk 取字段不对劲先检查是不是颜色转义码混进去了。我的习惯是凡是 grep 后面还要接别的命令一律加上--colornever。最后是二进制文件。grep 检测到文件里有 NUL 字节会默认把它当二进制处理只告诉你Binary file matches。如果日志里因为乱码混入了空字符加-a强制按文本处理即可。3. sed模式空间里的边读边改运维批量改文件的默认选择3.1 为什么叫流编辑器模式空间才是核心sed 的全称是 stream editor流式编辑器。它的工作方式像一条传送带读一行处理一行输出一行再读下一行。它不会把整个文件一次性加载到内存也不会像 vim 一样要求你打开文件再手动保存。sed 最核心的概念叫模式空间pattern space。你可以把模式空间理解成一个只有一行的临时工作区。sed 执行sed s/foo/bar/ file时实际上是从文件读入一行放进模式空间对模式空间里的这行执行你给的所有命令把模式空间的内容输出到标准输出清空模式空间读下一行。这个机制解释了为什么 sed 改文件是高效且可控的内存占用恒定处理逻辑线性。理解模式空间比记住几十个 sed 命令都重要——后续你看到sed -n 1,5p、sed /^#/d这些写法会立刻明白它们在哪一行上做了什么。3.2 地址定址sed 语法里最核心的骨架sed 的命令格式通常是这样[地址] 命令地址决定了命令作用在哪些行上。没有地址命令作用在所有行有地址就只作用于符合条件的行。地址支持三种形式# 1. 按行号 sed -n 1,5p file # 只输出第 1 到第 5 行 # 2. 按正则 sed -n /ERROR/p file # 只输出包含 ERROR 的行 # 3. 行号 正则混合 sed -n 1,/^server_name/p conf.conf # 从第 1 行到第一个 server_name 行还有一个容易被忽视的步进语法sed -n 1~2p file表示从第 1 行开始每隔 2 行输出一次也就是输出奇数行。同理2~2p是偶数行。有时候你想对日志做抽样检查比如只抽偶数行来看这个写法比awk NR%20更简洁。理解了地址很多看起来奇奇怪怪的 sed 命令就有了逻辑。sed -i /^#/d file的意思是地址是所有以 # 开头的行命令是删除合起来就是删掉所有注释行。3.3 核心命令p、d、s、y、a/i/c 一张表记下来sed 的常用命令没那么多我把它们分成增删改查四类# 查p打印模式空间内容必须配 -n 才有实际意义 sed -n /ERROR/p app.log # 删d删除模式空间内容然后进入下一行 sed /^$/d file # 删除空行 sed /^#/d file # 删除注释行 # 改s替换最常用的命令 sed s/foo/bar/ file # 每行第一个 foo 换成 bar sed s/foo/bar/g file # 每行所有 foo 换成 bar # 字符映射y把字符逐个替换 sed y/abc/xyz/ file # a-x, b-y, c-z # 增a行后追加、i行前插入、c整行替换 sed 5a # inserted file # 第 5 行之后追加一行 sed 5i # inserted file # 第 5 行之前插入一行 sed 5c # replaced file # 第 5 行整体替换y命令容易被忽略但处理大小写转换时很高效。比如sed y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/可以把整个文件小写转大写。当然大部分场景你直接用tr也行但 sed 的好处是能和地址定址配合只转换特定行。s命令还有一个重要变体替换分隔符可以自定义。处理包含大量/的路径时如果还用/做分隔符写出来是sed s/\/usr\/local\/bin/\/opt\/bin/g满天飞的反斜杠看得头大。改成#或|做分隔符一下就清爽了sed s#/usr/local/bin#/opt/bin#g file这是经验之谈只要替换内容里有特殊字符先换分隔符不要硬跟反斜杠较劲。3.4 实战一批量把配置文件里的旧地址换成新地址多台服务器上都有同一个配置文件里面写了旧的服务地址现在要全部改成新地址。逐台改太慢用 sed 批量处理# 先看一眼修改后的效果不加 -i输出到屏幕 sed s#old-service.example#new-service.example#g /etc/某服务.conf # 确认无误后原地写入 sed -i s#old-service.example#new-service.example#g /etc/某服务.conf这里提醒一句批量操作多台机器前最好先在一台上跑完diff验证再推到其余机器。如果环境有 Git 之类的版本管理改完立刻看 diff 是最稳的。3.5 实战二清掉所有注释和空行让配置瘦身有的配置文件里注释和空行比有效配置还多不删掉会影响脚本解析。一行搞定sed -i /^#/d; /^$/d file注意这里用了分号把两个 sed 命令串起来。sed 允许用分号在一个表达式里执行多条命令等价于sed -i -e /^#/d -e /^$/d file遇到行首可能有空格的情况得把正则在写严一点/^[[:space:]]*#/d否则只匹配行首绝对没有空格的注释行。3.6 sed -i 的隐藏坑GNU 和 BSD 的行为差异-i参数有个著名的坑Linux 上绝大多数发行版自带的是 GNU sedsed -i s/a/b/ file直接改原文件没问题但如果你在 macOS 这类 BSD 系环境跑同样的命令会直接报错因为 BSD sed 要求必须传备份后缀哪怕为空。BSD 系的正确写法是sed -i s/foo/bar/ file这个差异导致很多人写的脚本在 Linux 上跑得好好的放到另一台服务器上就挂。我的建议是除非你确定运行环境否则脚本里不要直接用sed -i改为先输出到临时文件再移动或者显式加.bak后缀。另外sed -i还有一个容易被忽略的行为GNU sed 默认会先写到临时文件再替换原文件这会影响文件的 inode如果你处理的是符号链接不加参数时它会解引用符号链接、改到真实目标文件上。在特殊场景下这是灾难所以要养成先用sed不带-i跑一遍看输出对不对再加-i的习惯。4. awk字段处理才是一切统计的基本功4.1 awk 的编程模型不是命令是一门小语言如果说 grep 是筛选器、sed 是编辑器那 awk 更接近一门小型的领域专用语言。它的核心模型是读入一行 → 按分隔符拆成字段 → 按条件匹配 → 执行动作。awk 对每一行的处理可以分成三个阶段BEGIN { 在读取任何输入之前执行一次 } { 对每一行都执行 } END { 在所有输入读取完之后执行一次 }学习 awk 的时候先把这三个箭头记清楚。你写的awk {print $1} file实际上就是中间那个对每一行执行的块省略了条件和 BEGIN/END。awk 里特殊变量$0表示整行$1表示第一个字段$2表示第二个字段以此类推。这是它和 grep/sed 最本质的区别grep 和 sed 的操作单位是一整行文本awk 的操作单位是行内被拆开的字段集合。4.2 内建变量FS、OFS、NR、NF 一网打尽awk 有一批内建变量搞懂它们你就能做各种报表变量含义例子FS输入字段分隔符默认空白awk -F: {print $1} file按冒号拆字段OFS输出字段分隔符默认空格awk -F, BEGIN{OFSRS输入记录分隔符默认换行awk BEGIN{RS;} {print $1}NR当前已读取的记录数也就是行号awk NR5{print $0} file输出第 5 行NF当前行有多少个字段awk {print NF, $NF}输出字段数和最后一个字段最常用的是-F指定分隔符。处理冒号分隔的文件# 输出用户名和 uid awk -F: {print $1, $3} 某用户文件注意$NF表示最后一个字段这在日志处理里极其有用。比如访问日志的最后一列经常是响应耗时如果你想统计最慢请求直接awk {print $NF}就拿到了耗时列完全不用关心总共有多少列。4.3 模式和动作条件和花括号组合成完整逻辑awk 的基本语法是条件 { 动作 }条件可以省略省略表示对每一行都执行。动作可以省略省略时默认输出整行。所以awk /ERROR/ file等价于grep ERROR file但请不要在只想匹配的时候用 awk——那是拿大炮打蚊子。稍微组合一下awk 的威力就出来了# 只处理包含 ERROR 的行输出第 1 个字段 awk /ERROR/ {print $1} app.log # 只处理行号在 10 到 20 之间的行 awk NR10 NR20 {print $0} app.log # 第三列数值大于 100 的行 awk $3 100 {print $1, $3} data.txt这里要特别提醒一个新手容易混淆的地方在 awk 里$3 100这种条件中$3是当前行的第三个字段而不是 shell 变量。4.4 实战日志统计三板斧把 awk 用回本我把日常中用得最多的三个 awk 场景列出来几乎每周都会用到。场景一统计各种状态码出现的次数。awk {cnt[$9]} END {for (code in cnt) print code, cnt[code]} access.log假设访问日志第 9 列是 HTTP 状态码。这段脚本的意思是每读一行就把该行状态码对应的计数器加一全部读完进入 END 块遍历所有状态码并打印。awk 的数组是关联数组用起来特别顺手。场景二统计所有请求的平均耗时和最大耗时。awk {sum $NF; if ($NF max) max $NF} END {print avg:, sum/NR, max:, max} access.logsum $NF累加每次请求耗时if记录最大值最后用已读行数NR求平均。这三行代码就是一个小型性能报告。场景三按小时聚合访问量。awk {split($4, a, :); cnt[a[2]]} END {for (h in cnt) print h, cnt[h]} access.log | sort访问日志里时间戳的格式类似[05/Feb/2025:14:03:22 0800]$4就是这个字段。split($4, a, :)按冒号拆开a[2]就是小时。统计完再用sort排序输出。是不是比写一堆 Python 去解析方便得多4.5 新手常踩的坑$符号、引号、shell 变量传递新手用 awk至少会在三个地方卡壳。第一个是$符号的语义。在 awk 表达式里$1是字段引用但$NF中的NF是内建变量所以$NF是取第 NF 个字段的意思。有人会写成$NF想表示最后一个字段这没问题但如果你写awk {print $1, $NF}最后两个字段会打印两次。注意区分。第二个是 shell 变量怎么传进 awk。很多人的第一反应是直接拼进单引号# 错误示范 keyERROR awk /$key/ {print} app.log在单引号包裹的 awk 脚本里$key会被 awk 理解成字段 key而不是 shell 变量 key 的值。正确做法是用-v传变量keyERROR awk -v k$key $0 ~ k {print} app.log第三个是引号嵌套。awk 脚本本身要用单引号包住脚本里涉及字符串比较时字符串要用双引号。比如awk $1 ERROR。如果你在 shell 里已经用了双引号包住 awk 脚本脚本内部的字符串又要再小心处理层级一多就容易乱。我的习惯是awk 脚本永远用单引号包内部字符串用双引号绝不混用。这样层级清晰基本不会出错。5. 三剑客联动从一份真实访问日志中榨出完整结论5.1 场景设定日志长什么样我们要回答什么问题直接上一个真实感很强的场景。假设我有一个 web 应用访问日志access.log的格式是常见的组合格式每一行长这样192.0.2.11 - - [05/Feb/2025:14:03:22 0800] GET /api/v1/list HTTP/1.1 200 4521 0.023 198.51.100.7 - - [05/Feb/2025:14:03:25 0800] POST /api/v1/upload HTTP/1.1 500 231 0.512 203.0.113.9 - - [05/Feb/2025:14:03:28 0800] GET /api/v1/detail HTTP/1.1 200 1200 0.009字段说明第 1 列是客户端 IP第 4 列是带方括号的时间戳第 5 列到第 8 列是请求行第 9 列是状态码第 10 列是返回字节数第 11 列是响应耗时秒数。现在我要回答三个问题访问量最高的 TOP 10 IP 是哪些返回 500 的请求集中在哪个时段请求最慢的时段和平均耗时是多少5.2 第一步grep 负责切出关注窗口先不急着上 awk。几万行日志全部丢给 awk 做统计也能算但如果我们只关心 500 错误就应该先用 grep 把 500 的行捞出来缩小数据量。这个动作叫切面。# 只保留状态码为 500 的行 grep 500 access.log err500.log注意我搜的是 500带了一个前引号和空格。为什么不是简单搜500因为日志里处处可能有 500比如返回字节数是 500、耗时是 0.500都会干扰。要把状态码列精确匹配带上引号和空格是最快的做法。这是 grep 实战中很容易被忽略的细节搜索词越贴近字段边界结果越干净。5.3 第二步sed 负责清洗字段格式日志里的时间戳带着方括号和斜杠日期如果要按小时聚合这个原始格式不太好直接统计。先用 sed 把时间戳里的杂讯处理一下。比如我要把时间戳统一化简去掉方括号和日期部分只留下时钟部分sed s/\[[0-9][0-9]\/[A-Za-z]*\/[0-9][0-9][0-9][0-9]:// err500.log跑完一行就变成了192.0.2.11 - - 14:03:22 0800] GET ...这只是一个清洗示例。真实场景里你可能还需要去掉请求行的引号、把逗号替换成空格等等。清洗的目的是让后面的 awk 能用统一的分隔符轻松拆字段。这一步很容易被初学者跳过但跳过以后 awk 字段全错才是真麻烦。5.4 第三步awk 负责聚合统计输出报表如果只是想看 500 错误的分布和 TOP IP直接对 err500.log 做统计awk {ip[$1]} END {for (i in ip) print ip[i], i} err500.log | sort -rn | head -10解释一下ip[$1]用 IP 做 key 计数END块里遍历数组打印次数和 IPsort -rn按第一列数值降序head -10取前十条。为什么不用sort | uniq -c因为 awk 在单次扫描里就完成了计数效率更高用sort | uniq -c还得先排序再相邻去重多一趟。同样查 500 错误集中在哪个小时grep 500 access.log | awk {split($4, a, :); cnt[a[2]]} END {for (h in cnt) print h, cnt[h]} | sort -n输出类似14 36 15 89 16 12一眼就能看到 15 点出现了异常高的 500 错误数。5.5 完整命令存档一次管道三个问题一次答完把上面的思想合并成一条命令其实也不用在中间落盘临时文件grep 500 access.log \ | sed s/\[[0-9][0-9]\/[A-Za-z]*\/[0-9][0-9][0-9][0-9]:// \ | awk {split($4, a, :); ip[$1]; hour[a[2]]} END {for (i in ip) print IP, i, ip[i]; print ---; for (h in hour) print HOUR, h, hour[h]} \ | sort -k2,2rn | head -30这条命令看起来很复杂但拆开看就是前面三步grep 切面sed 清洗awk 统计。很多老运维能写出很长的管道命令靠的不是背而是把每个环节的责任划分清楚——哪一段负责缩小数据量哪一段负责格式化哪一段负责计算。管道思维的本质就是把一个大问题拆成小步骤每个步骤用最合适的工具。写完后哪怕几个月不碰回头看仍然能读懂每一步在干嘛。6. 底层逻辑与性能边界知道快在哪才知道什么时候别硬扛6.1 流式处理的本质内存占用恒定适合大文件三剑客都是流式处理模型也就是边读边处理。无论输入文件是 1MB 还是 10GB它们的内存占用基本都保持在一个很低的水平只随单行长度变化不随总行数变化。这一点在运维场景里是压倒性的优势。我经常要在几个 GB 的日志文件上做统计如果没有流式工具就得先把整个文件读进内存服务器分分钟被打爆。三剑客加一个管道不落盘、不加载全量几十秒出结果。不过要区分grep 和 sed 是纯流式的内存好控awk 因为有关联数组如果把几亿个不同的 key 都塞进数组内存会跟着 key 数量增长。比如统计一整天所有用户 ID 的访问次数如果用户量有两亿内存就会明显上去了。所以 awk 数组也不是无限免费的。6.2 三个工具的性能差异与选择我之前在部分场景里做过简单对比结论基本符合预期任务类型数据量推荐工具备注纯关键字过滤5GB 日志grep比 awk 过滤快得多因为不需要拆字段整行字符串替换1GB 文本sed线性扫描速度稳定按列统计汇总1GB 日志awk扫描一次同时累加多个统计值复杂正则 字段处理几百 MBgrep -o 或 awk先 grep 缩小数据再交给 awk我在实际中惯用的原则是能用 grep 先过滤的绝不让 awk 处理全量数据。awk 虽然也能做过滤但它多付了一份拆字段的成本。grep 过滤完之后的数据可能只有原来的 1/10后面的 awk 就轻松多了。6.3 三剑客的边界结构化数据、复杂关联、可维护性流式处理不是万能的。awk 再强本质上还是在处理按行组织的文本遇到 JSON 嵌套结构、多级数组、跨文件 joinawk 写起来不仅很痛苦而且容易出错。举一个典型的例子日志里有一段 JSON{user: u123, items: [{sku: a, count: 2}, {sku: b, count: 5}]}如果你想用 awk 把 user 和 items 里的 count 全部提取出来再做统计正则和字段切分会非常绕。这种结构化数据直接用 jq 几行搞定没必要为了三剑客情怀硬扛。我的判断标准很简单数据是行导向还是层级导向。行导向的文本三剑客高效层级导向的结构化数据用专用解析器。另外还要考虑可维护性。一条三五个工具的管道命令在终端里跑当然爽但如果你要把它写进自动化脚本、让其他人维护你就得考虑这个复杂管道是不是该收敛成一个脚本awk 脚本写长了更清晰的做法是把它单独写成.awk文件而不是全部塞在管道里。6.4 一个反面例子管道里的每行启动一个进程性能上最反感的写法是在 awk 里对每一行调用system()或 shell 命令。比如# 反面教材对每一行执行一次命令几万行就启动几万个进程 awk {system(echo $1)} access.log每次system()都要 fork 一个新进程数据一大性能直接崩塌。同样的需求应该写成awk {print $1} access.log把输出交给管道里的其他命令而不是在 awk 内按行调外部命令。这个毛病在刚接触 awk 的脚本里特别常见记住了能避免把一台机器拖垮。7. 避坑清单与学习路径建议7.1 正则差异BRE、ERE、awk 里又是另一套正则的坑我在三剑客里都碰过。总结下来就是一句话别以为 Linux 下的正则都是同一种。grep 默认是 BRE-E切到 ERE-P可以切到 PCRE可选。sed 默认也是 BRE所以你在 sed 里写\或者\( \)都是常规操作。awk 用的是它自己的 ERE 风格同时又和 POSIX ERE 有细微差别。最典型的坑是花括号# BRE 下写重复次数要转义 grep a\{3\} file # ERE 下直接写 grep -E a{3} file # awk 里花括号是块标记正则里要用 /a{3}/ awk /a{3}/ {print} file还有一个很容易被忽略的细节在 BRE 下\|表示或\(\)表示分组但在 ERE 下它们反过来不需要转义。如果你在一个用了大量转义的正则里漏掉一个反斜杠结果可能是灾难性的——不是报错而是静默匹配到一个错误模式。所以我的习惯是正则一复杂优先 grep -E 和 sed -EGNU sed 支持 -E把转义噪声降到最低。awk 的正则则单独记忆千万别把 awk 的分组语法和 grep -E 完全划等号。7.2 引号与转义单引号、双引号、反斜杠的层层关系shell 引号是一个永恒的话题尤其在管道命令里。我把常见规则说透单引号内所有内容都是字面量shell 不会做变量展开双引号内变量会展开但$1这种对 awk 来说是字段引用shell 不会去动它在双引号包裹的命令里反斜杠有时是给 shell 看的有时是给工具看的层级一多极易出错。我自己的操作习惯是awk 和 sed 的脚本用单引号包住然后里面用双引号表示字符串如果需要传入 shell 变量用-v传给 awk而不是试图把变量拼接进正则。这个习惯帮我避免了大量引号地狱。处理路径替换时我推荐多用自定义分隔符来减一层转义# 不推荐反斜杠满天飞 sed -i s/\/var\/log\/app/\/data\/logs\/app/g file # 推荐用 # 做分隔符 sed -i s#/var/log/app#/data/logs/app#g file7.3 -i 的安全操作习惯先看输出再看 diff最后写回我见过不止一次有人写完sed -i手一抖把线上配置文件改错了还没有备份。这里分享我的固定流程# 第一步先看不加 -i 的输出 sed s/old/new/g /etc/某服务.conf # 第二步如果确认没问题加 -i 并附带备份后缀 sed -i.bak s/old/new/g /etc/某服务.conf # 第三步确认备份和改动再用备份做差异对比 diff /etc/某服务.conf.bak /etc/某服务.conf有时候改动量很大我还会先grep -c看看命中行有多少估算改动范围。如果在生产环境批量改服务器我甚至会先在备用机器上跑完整流程再把脚本带到线上执行。这个习惯救过我很多次。7.4 学习路径建议从会背参数到管道思维最后聊聊怎么系统地把三剑客学到手。第一阶段先把三件事练熟grep -E配合-n/-A/-B/-c/-o查日志sed s/old/new/g做替换awk {print $1, $NF}取字段。这三板斧覆盖了 80% 的日常需求练到不用想就能写。第二阶段练习组合拳。找一份真实的访问日志自己设计几个问题——TOP IP、状态码分布、慢请求时段、按模块统计错误数。每个问题都用grep 过滤 sed 清洗 awk 统计的流程写出来。这个阶段你会开始意识到每个工具都有自己的强项组合思考比背命令更重要。第三阶段深入底层细节。搞懂正则引擎在 BRE/ERE 下的差异搞懂 awk 的字段和内建变量机制搞懂 sed 的模式空间。这时你再看别人的命令行已经能从他在用什么命令进阶到他为什么要这样做。三剑客是越用越熟练的工具深入理解之后你遇到的每一个文本处理问题脑子里都会自动浮现出一条管道。