ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Shell文本处理三剑客:grep、sed、awk核心用法与实战

Shell文本处理三剑客:grep、sed、awk核心用法与实战 刚入行做运维那会儿我一直觉得 Shell 里能用的文本处理工具无非就是 grep 一把梭。日志搜不到就多套几层管道配置文件要改就手动打开 vim 去编辑需要整理数据就复制到 Excel 里折腾半天。直到有一天一个两千多行的配置文件要在十分钟内完成批量修改、一个上 GB 的日志要在秒级内把异常请求的频率统计出来我才被迫认真把 grep、sed、awk 这三件套系统地学了一遍。学完之后最大的感受是这三样东西不是锦上添花的技巧而是 Shell 脚本编写绕不开的基础设施。这篇内容就是把我在实际项目里用这三件套的经验、踩过的坑、以及背后的取舍逻辑一次讲清楚。无论你是刚接触 Shell 的新手还是已经写了几年脚本但主要靠复制粘贴的熟练工这篇内容都能让你对它们有一个不浮于表面的理解。1. 文本处理三剑客的真实定位它们能解决什么不能解决什么很多人把 grep、sed、awk 统称为三剑客但说实话这个称呼容易让人误以为它们是三个功能重叠的工具。实际上它们的定位非常清晰且互补理解这个分工逻辑才是用好它们的前提。1.1 三个工具的分工逻辑先看一个最简单的文件内容假设有个/etc/passwd的简化版本root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin webuser:x:1001:1001:web:/home/webuser:/bin/bashgrep 负责找它只做一件事就是按行匹配文本输出符合条件的行。它不会修改文件不会提取字段不会汇总统计。sed 负责改它是一个流编辑器擅长对文本流进行替换、删除、插入等操作。它处理的是行这个单位但可以精确控制行范围和模式。awk 负责算它把每一行按分隔符切分成字段支持条件判断、循环、数组、内置函数本质上是一门微型编程语言适合做结构化文本的提取、转换、统计。用一句话概括它们的关系grep 解决的是哪些行关心sed 解决的是怎么改这些行awk 解决的是这些行里的字段怎么处理。这三者不是竞争关系而是上下游关系。实际项目里最常见的组合套路是先用 grep 把关心的行筛出来再用 awk 提取字段必要时在过程中用 sed 做文本替换。1.2 什么时候不该用它们这一点是很多教程不会讲的但恰恰是经验里最值钱的部分。三剑客虽然强大但并非所有场景都适合。文件超过几个 GB 时三剑客仍然可用但要小心。grep 和 sed 是流式处理内存占用低但在大量数据上来回跑多次会产生明显的 IO 开销。如果只需要统计某个字段的总和awk 一遍扫描就能完成就别先 grep 再 awk 分两步来做。遇到 JSON、XML 这类嵌套结构数据时别硬用正则。JSON 的括号嵌套层级一深正则表达式根本没法可靠解析。这种情况下应该用jq、python或其他专门工具而不是拿着一个超长正则去硬碰。我见过不少人在日志里提取 JSON 字段时写了一个 80 多字符的正则运行倒是能跑但一遇到键顺序变化就崩最后老老实实改成 python 一行命令解决。工具选错不是能力问题是认知问题。如果是跨平台脚本尤其是 macOS 和 Linux 之间sed 的语法差异要格外谨慎。macOS 自带的 sed 是 BSD 版本很多参数行为和 GNU sed 不一样。典型的例子是sed -i的用法GNU sed 支持sed -i s/old/new/g file而 BSD sed 要求必须有备份后缀写成sed -i s/old/new/g file。一个引号之差脚本在 Mac 上直接报错。这不算 sed 的缺点但使用时要心里有数。日常简单替换用 perl 或 python 更省心的情况也存在。比如需要跨行匹配、需要处理二进制内容、或者需要复杂的前后关联逻辑时sed 的单行模式就力不从心了。我的原则是单行内能搞定的逻辑交给三剑客需要上下文关联、状态机的逻辑直接交给 python。工具是用来解决问题的不是用来表忠心的。2. grep从关键字匹配到精准过滤的完整用法grep 看起来简单实际用起来有很多容易被忽略的细节。这里把我在脚本和日常操作中最常用的 grep 用法完整梳理一遍。2.1 基础匹配与返回码的作用grep 的核心逻辑是逐行读取输入用模式去匹配匹配成功就输出该行。最简单的用法大家都会比如grep error app.log。但脚本编写者必须关注一个容易被忽略的东西——grep 的返回码。grep返回 0 表示至少匹配到一行返回 1 表示没有匹配到任何行返回 2 表示文件不存在或读取失败。这个返回值在 Shell 脚本的条件判断里非常有用if grep -q ERROR /var/log/app.log; then echo 日志中存在 ERROR else echo 日志中没有 ERROR fi-q参数表示静默模式不输出内容只影响返回码。在脚本逻辑判断里配合-q使用比grep ... /dev/null 21写得干净利落。这一点我见过很多人忽略还在用老办法把输出重定向到黑洞其实-q就是官方推荐的静默方式。另外还有一个细节grep 的返回码在管道里会被管道中最后一个命令的返回码覆盖。比如grep ERROR app.log | wc -l这个管道返回的是wc -l的状态不是 grep 的状态。要拿到 grep 的状态得借助PIPESTATUS数组bash 环境grep -q ERROR app.log | wc -l echo ${PIPESTATUS[0]} # 输出的是 grep 的返回码这在脚本排错时特别有用。有一次线上脚本明明日志里没有匹配内容却因为管道后一个命令执行成功而误判流程继续往下走最后排查到就是这个返回码的坑。2.2 实际场景中的标志组合以下是几个高频组合场景我用项目里真实的命令来展示。递归搜索一个目录下的所有文件并且显示文件名和行号grep -rn timeout /etc/nginx/ --include*.conf-r递归子目录-n显示行号--include*.conf只搜.conf文件这个命令在排查配置问题时非常常用。比如说上线的 nginx 配置里有超时设置不生效先全局搜一下所有timeout出现在哪些文件第几行比一个个打开文件查看要快得多。搜索时忽略大小写grep -ri error /var/log/ --exclude*.gz-i忽略大小写--exclude*.gz排除压缩日志。如果不排除压缩文件grep 会把二进制乱码也读一遍输出内容根本无法阅读。匹配整个单词而不是子串grep -w cat text.txt-w表示匹配完整的单词只匹配cat不会匹配concatenate、catalog。这个参数在处理代码变量名时能救命。我见过有人在配置文件里搜debug结果把所有debug_mode、debug_level都匹配出来最后用-w才发现真正生效的其实是debug这个开关。显示匹配行的上下文grep -n -B 5 -A 10 OutOfMemoryError app.log-B 5匹配行之前 5 行-A 10匹配行之后 10 行在排查异常日志时单独看一行报错根本不够必须看上下文才能判断触发原因。这个参数组合是我个人使用频率最高的 grep 命令之一。多个模式同时匹配grep -E ERROR|FATAL|WARN app.log-E启用扩展正则表达式让|作为或操作符。如果不用-E就得写成grep -e ERROR -e FATAL -e WARN这种冗余形式。-E和egrep等价但推荐前者因为 egrep 在 POSIX 标准里已经被废弃了。反向匹配grep -v ^# /etc/ssh/sshd_config-v输出不匹配的行即反选。上面这个命令用来过滤掉配置文件里的注释行。实际看配置时先用grep -v ^#把注释丢掉再配合grep -v ^$去掉空行剩下的才是有效配置。2.3 grep 与正则表达式的边界grep 支持三种正则模式基本正则BRE默认、扩展正则ERE使用-E、以及 Perl 兼容正则PCRE使用-P。三者的区别主要是元字符是否需要转义。比如在基本正则里(、)、{、}、、?这些字符都需要加反斜杠才表示特殊含义而在扩展正则里直接写就行。举个例子# 基本正则匹配连续出现 3 次的数字 grep \(123\)\{3\} file.txt # 扩展正则同样的效果 grep -E (123){3} file.txt我个人的建议是脚本里统一使用-E理由很实在——更容易阅读和维护。-P的 PCRE 功能最强大支持零宽断言、非贪婪匹配等高级特性但并不是所有 grep 版本都支持macOS 的 BSD grep 默认不支持-P。在写可移植脚本时尽量避免依赖-P否则换一台机器可能就无法运行。一个常见的坑模式中的变量。如果你在脚本里用 grep 匹配一个包含特殊字符的变量比如 IP 地址192.168.1.1正则里的.能匹配任意字符结果可能超出预期。解决方法是把.转义或者用grep -F固定字符串匹配ip192.168.1.1 grep -F $ip /etc/hosts-F会把整个模式当作字面字符串来处理不做正则解析。在脚本中处理用户输入或动态变量时-F是避免正则误匹配的可靠手段。这一条经验来自于一次真实的事故脚本里匹配版本号版本号里的点号因为正则被当成通配符误匹配了一堆无关行导致后续处理数据错乱。3. sed流式编辑器的修改逻辑与常用操作sed 的全称是 stream editor流编辑器。理解流这个概念是掌握 sed 的钥匙。3.1 面向流而非面向文件大多数编辑器是面向文件的打开文件、修改、保存。sed 不是这样它逐行读取输入可以来自文件也可以来自管道对每一行执行编辑命令然后输出结果。它不修改原文件除非用-i只把处理后的结果输出到标准输出。这个特性的意义在于sed 非常适合在管道中工作。比如查询某个进程的 PID 并把它提取出来pgrep -f java.*app.jar | sed s/^/PID: /每一行都会被加上PID:前缀然后输出。整个过程不需要中间文件数据像流水一样经过 sed 后继续往下游流。这就是流的含义。sed 的基本执行流程可以用一个简单的模型来描述读取一行到模式空间pattern space执行命令输出模式空间的内容清空再读下一行。命令可以作用于所有行也可以限定在满足某个条件的行上。3.2 替换、删除、插入最常用的三类操作替换是 sed 的核心操作。基本格式是sed s/old/new/ file.txt默认只替换每行第一个匹配。要替换所有匹配必须加g标志sed s/old/new/g file.txt区别在实践中的效果非常明显。比如有一个文件每行有多个空格需要统一改成单个|echo a b c d | sed s/ \/|/g输出结果是a|b|c|d。如果没有g结果是a|b c d只会替换第一个匹配。sed 的替换支持后向引用这是它做文本重组的利器。假设日志里有一行格式是时间|级别|消息想调整顺序echo 2025-01-15|INFO|started | sed s/\([^|]*\)|\([^|]*\)|\(.*\)/\3|\2|\1/输出started|INFO|2025-01-15。这里\(...\)是捕获组后面的\1、\2、\3就是对捕获内容的引用。注意在基本正则下括号必须转义所以写起来比较啰嗦。使用sed -E可以简化echo 2025-01-15|INFO|started | sed -E s/([^|]*)|([^|]*)|(.*)/\3|\2|\1/和 grep 一样-E让正则更可读。我强烈建议在 Sed 命令里也用-E。删除操作d命令删除匹配的行。sed /^#/d file.txt # 删除以 # 开头的行 sed 3,5d file.txt # 删除第 3 到第 5 行 sed /^$/d file.txt # 删除空行这三种删除方式在配置文件清理时极为常见。比如清理 nginx 配置里的注释和空行sed -E /^[[:space:]]*#/d; /^$/d /etc/nginx/nginx.conf[[:space:]]*#表示可能包含前导空白空格或 tab的注释行。真实的配置文件往往带缩进直接匹配^#会漏掉缩进的注释。插入和追加i在匹配行之前插入a在匹配行之后追加。sed /^server {/i # new block /etc/nginx/nginx.conf sed /^server {/a # end block /etc/nginx/nginx.conf这个在实际中用于给配置模板加标注很有用但语法要求比较细插入的文本如果包含空格在 GNU sed 里直接用在 BSD sed 里则要求每行以反斜杠结尾。跨平台脚本建议用printf拼接命令或者直接改用 python 处理。3.3 按范围处理sed 真正拉开差距的地方sed 最强悍的地方在于可以按范围处理。范围有多种形式按行号范围sed -n 10,20p file.txt # 打印第 10 到 20 行注意这里用了-n它的作用是关闭默认输出。如果不加-nsed 会先输出所有行再输出匹配的行结果就是重复内容。-n和p通常配对使用表示只打印匹配的行。按模式范围sed -n /BEGIN/,/END/p file.txt这个命令会从匹配BEGIN的行开始一直打印到匹配END的行结束。这是提取日志中某个事务上下文的利器。比如一个请求的日志从 REQUEST START 开始到 REQUEST END 结束中间夹杂着其他请求的日志用这个命令就能干净地把目标请求的全部日志捞出来。行号和模式的组合sed -n 1,/^$/p file.txt # 从第 1 行到第一个空行这在提取文件的头部配置区时很常用。许多配置文件头部是全局配置遇见第一个空行就结束这种范围写法刚好匹配。范围配合替换操作在特定区域内做替换而不是全文件替换。sed -i /config/,/\/config/ s#http://#https://#g app.xml这个命令把config和/config之间的http://全部替换为https://。注意这里用了#作为分隔符因为 URL 本身包含/如果用/做分隔符就得转义写一堆\/可读性很差。sed 的分隔符可以换成任意字符这是实践中非常实用的小技巧。-i参数使用时的建议直接修改原文件时加-i。但我在实践中有一个习惯先用不带-i的命令确认输出结果正确确认无误后再加-i执行真正的修改。如果确实要一步到位建议利用-i.bak先生成备份sed -i.bak s/foo/bar/g config.txt执行后会产生config.txt.bak备份文件修改出问题时可以快速回滚。这个习惯在修改生产环境配置时能救命。4. awk字段处理与结构化输出如果说 grep 和 sed 是行级别的工具awk 就是列级别的工具。awk 把一行文本拆成多个字段然后按字段做各种处理。对于日志分析、表格数据转换、统计汇总这些场景awk 的效率和表达能力远超手动拼 shell 脚本。4.1 awk 的awk列提取的重中之重awk 默认按空白字符空格、Tab切分字段每行切分后可以用$1、$2、$3引用第 1、2、3 个字段$0表示整行。NF表示每行的字段数量。最简单的提取操作awk {print $1, $3} /var/log/nginx/access.log假设日志格式是IP - - [日期] GET /path HTTP/1.1 200 1024那么$1是 IP$9是状态码$10是响应体大小。默认分隔符在很多场景下不够用比如/etc/passwd用冒号分隔。这时用-F指定分隔符awk -F: {print $1, $3, $7} /etc/passwd这条命令提取用户名、UID、登录 shell。如果文件有多种分隔符比如同时有空格、冒号、逗号-F支持正则表达式awk -F[ ,:] {print $1, $2} data.txt中括号里的字符表示分隔符集合表示连续出现多个分隔符当作一个来处理。这个用法在解析混合分隔符的文本时非常高效。4.2 内置变量和条件过滤awk 内置了FS字段分隔符、OFS输出字段分隔符、NR当前行号、NF字段数量、RS记录分隔符等变量。这些变量既可以在命令行通过-v设置也可以在 awk 代码里直接使用。条件过滤是 awk 最常见的场景之一。格式是awk 条件 {动作}只输出状态码为 500 的请求行awk $9 500 {print} /var/log/nginx/access.log条件可以是数字比较、字符串比较、正则匹配。调用痕迹可以在动作前方加条件不加条件的动作在每行都会执行。行号变量 NR 的用途打印第 10 到第 20 行awk NR 10 NR 20 {print NR, $0}在 awk 里做行号过滤比 sed 直观得多尤其是需要同时输出行号和行内容的富文本场景。字符串拼接awk 里可以用把字段拼起来。把 IP 和时间字段拼成新格式awk {print $1 - $4} access.logOFS变量可以将输出分隔符统一配置。设置OFS后print输出多个字段时用自己的字符分隔awk -F: -v OFS| {print $1, $3, $7} /etc/passwd输出为root|0|/bin/bash。这在生成 CSV 或 TSV 格式数据时很实用。4.3 进阶场景数组与统计汇总awk 的数组用起来像一个关联映射字典可以在内存里聚合数据。这是它做统计汇总的核心能力。统计访问量最高的前 10 个 IPawk {count[$1]} END {for (ip in count) print count[ip], ip} access.log | sort -rn | head -10这段代码的整个过程是每读一行count[$1]这个数组以 IP 为下标累加 1处理完所有行后在 END 阶段遍历数组输出计数和 IP然后交给sort -rn按数字降序排序head -10取前 10 名。这就是 awk 的典型用法用数组做分组统计用 END 块做汇总输出。同样的逻辑还可以统计请求路径、状态码、User-Agent 等。按状态码统计数量awk {code[$9]} END {for (c in code) print c, code[c]} /var/log/nginx/access.log求和某列数据比如统计所有响应体大小之和awk {sum $10} END {print Total:, sum} access.log注意sum没有提前赋初值awk 默认数值变量初值为 0所以可以直接做累加。统计平均值awk {sum $10; n} END {print Avg:, sum/n} access.logn每行累加 1最后用总和除以行数。如果要排除某一列不是数字的行可以先判断字段值是否为数值$100 $10验证合法性。多条件统计的进阶用法统计/api接口的 5xx 错误数量awk $7 ~ /^\/api/ $9 500 {count} END {print count} access.log~是正则匹配运算符$7 ~ /^\/api/表示第 7 个字段请求行以/api开头。是与条件。这种组合条件写法在日志分析中非常常用。awk 的BEGIN块可以在读文件之前做初始化比如设置分隔符、打印表头awk BEGIN {print IP\tCount} {count[$1]} END {for (ip in count) print ip \t count[ip]} access.log | sort -t $\t -k2 -rn | head把BEGIN块加进来整个命令的自解释性更强。5. 三者组合与排错实践单个工具讲完之后看几个组合使用的真实场景。这也是三剑客最见功力的地方不是单个命令多花哨而是能像积木一样拼出解决复杂问题的管道。5.1 实战一分析 Nginx 访问日志统计客户端 IP 归属假设日志格式是$remote_addr - - [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent我要找出最近日志里所有的 5xx 错误并按 IP 统计错误次数同时列出每个 IP 最近一次错误的时间。一条管道搞定grep HTTP/1\.[01] access.log \ | awk $9 500 {count[$1]; last[$1]$4} END {for (ip in count) print count[ip], ip, last[ip]} \ | sort -rn | head -20分析一下每一步在干什么第一步grep过滤出 HTTP 请求行。第二步awk筛选状态码大于等于 500 的行按 IP 分组计数同时记录每个 IP 最后一个时间戳字段$4到last数组。第三步sort按错误次数降序排列。这个管道处理几百万行日志也只需几秒。如果只用一个 awk 也能实现同样的功能但 grep 先做粗筛会让 awk 处理的数据量小很多整体性能更好逻辑也更清晰。5.2 实战二批量修改配置文件并验证结果比如要把某个 Java 应用的所有配置里的数据库连接从jdbc:mysql://old-db:3306改成jdbc:mysql://new-db:3306同时只改测试环境用的配置文件。# 先预览将要修改的内容 grep -n old-db /etc/app/config/*.properties # 批量替换 sed -i s/old-db/new-db/g /etc/app/config/*.properties # 验证修改结果 grep -n new-db /etc/app/config/*.properties三步走先预览再执行再验证。这是我在生产环境修改任何配置时的固定流程。先看清楚改动会影响哪些行然后再动手不要直接跳过预览就直接替换。如果需要在替换前确认每个文件是否包含目标模式可以用一个条件判断for f in /etc/app/config/*.properties; do if grep -q old-db $f; then sed -i s/old-db/new-db/g $f echo updated: $f else echo skipped: $f (pattern not found) fi done这个脚本的巧妙之处先 grep 判断再 sed 修改避免了对没有匹配模式的文件进行无意义的写操作同时保留了文件的时间戳。5.3 调试技巧三剑客脚本出错时怎么查经验丰富的老手写三剑客命令也会出错。关键不在于不犯错而在于有一套调试方法。第一件事分步验证管道中的每一步输出。要把长管道拆开跑每一步单独看结果确认符合预期后再合起来。例如# 先看 grep 的输出 grep ERROR app.log | head # 确认无误后再加上 awk grep ERROR app.log | awk {print $1, $2} | head不要一把梭写完整条命令出错后无从下手。分步验证是最稳妥的做法。第二件事用head、wc -l验证数据量。管道跑完先别急着看全量结果用head -5看前几行用wc -l看总行数。数据量明显不对说明前面的过滤条件有问题。第三件事set -x检查脚本执行过程。在 Shell 脚本里调试时在开头加一句#!/bin/bash set -x执行时会打印每条命令的实际参数。变量有没有正确展开、引号有没有生效一目了然。在复杂脚本里排查时set -x比任何调试器都直接。第四件事注意命令的引号和转义。三剑客命令出错时最高频的原因不是逻辑问题而是引号问题。比如在单引号包裹的 awk 脚本里想用单引号就需要通过\x27转义或者改用双引号但双引号会让变量提前展开可能导致意外。有一个我常用的技巧把 awk 脚本写成临时文件用-f参数指定cat /tmp/count.awk EOF {count[$1]} END { for (ip in count) print count[ip], ip } EOF awk -f /tmp/count.awk access.log用 heredoc 写多行 awk 脚本既避免了引号地狱还能把复杂逻辑维护成文件。对于三行以上的 awk 脚本我都建议这么做。第五件事善用--debug-expression或printf调试。awk 里可以在关键位置加打印语句来确认中间状态awk {print DEBUG: field1 $1 , field3 $3 /dev/stderr; count[$1]} END {for (ip in count) print count[ip], ip} access.log调试信息输出到 stderr不影响正常的结果输出。这在调试复杂 awk 脚本时特别有用既能保留中间变量现场又不污染最终数据。组合工具的能力不在于记住多少命令参数而在于对数据流和文本结构的理解。grep 负责定位sed 负责变换awk 负责结构化处理把它们串起来能解决绝大多数日常运维和开发中的数据加工任务。写 Shell 脚本时不要一遇到文本处理就想着用 Python在命令行场景里三剑客的组合拳远比起一个 Python 进程更轻量、更直接。如果让我给一条最核心的学习建议那就是先吃透grep -E、sed -E、awk -F这三个组合的常用参数再通过三个实战场景把它们串起来就足够应付绝大多数真实需求。
返回列表