在Linux环境里做开发和运维,每天逃不开的一件事就是处理文本。不管是看日志、改配置、分析数据,还是写脚本批量处理,本质上都在跟一堆堆字符打交道。手里那点活儿,用鼠标和编辑器挨个翻,能把你累到怀疑人生;可要是你真正用好了grep、sed、awk这三个命令,效率直接翻几倍,而且写出来的Shell脚本会显得特别利落。这篇东西,我打算把这三个工具从底层逻辑讲到实战技巧,把我平时踩过的坑和积累的习惯一并交代清楚,希望给正在入门Shell脚本、或者写了几年脚本但总觉得"差点意思"的朋友一些真正能落地的参考。
先给这三个家伙定个位:grep负责"找",sed负责"改",awk负责"算和整理"。它们不是互相替代的关系,而是各管一段的黄金搭档。一个典型的场景是这样的——你拿到一份几十万行的日志,先grep把报错行筛出来,再用awk提取里面的IP、时间、耗时这些字段,最后用sed把格式整理成你想要的报表。三步走完,原本一上午的活儿,一条命令几秒钟就出结果了。这就是文本处理三件套的价值:把"体力活"变成"脑力活"。
1. 三件套的定位与整体设计思路
1.1 grep、sed、awk分别解决什么问题
很多人刚接触这三个命令时最容易犯的错,就是想用某一个搞定所有需求。实际上它们的定位完全不同。
grep全称是"global regular expression print",它的核心功能是按正则表达式筛选文本行。它像流水线上的质检员:只关心"这一行到底有没有我要的东西",有就留下,没有就丢掉。它不会改你的数据,只是"过筛子"。我平时用grep最多的场景就是日志分析和代码搜索,比如查某个接口什么时候开始报错、某段逻辑在哪个文件里出现过。
sed(stream editor,流式编辑器)的强项是"就地加工"。它能对匹配到的行做替换、删除、插入、追加操作。它像一个装配线上的改造工:经过它手里的每一行,都可能是全新的面貌。配置文件的批量改动是它最经典的用武之地,比如几十台服务器上的Nginx配置要统一改一个参数,手改得改到吐,sed一条命令全部搞定。
awk则是个"重器",它把每一行拆成字段(列),然后支持编程逻辑——变量、循环、判断、数组全都有。它像流水线尽头的数据分析员,把质检员和改造工送过来的数据做统计、汇总、格式化输出。像"统计Nginx日志里访问量Top10的IP""计算所有接口的平均响应时间"这类需求,awk写起来就是几行的事,你要是用Python跑,还得写文件读取、字段Splite,绕一大圈。
1.2 为什么它们是Shell脚本的基石
这三个工具的底层设计哲学,和Unix的经典理念完全一致:一个程序只做好一件事,然后通过管道协作解决复杂问题。一件复杂的文本处理任务,几乎都会被拆成一个管道链,比如:
bash复制cat app.log | grep "ERROR" | awk '{print $2}' | sort | uniq -c
这个管道链里,每个命令只负责一个环节,数据像流水一样经过它们,最后落到你手上。这种设计让Shell脚本拥有极强的组合性和可维护性——你想调整中间任何一环,不需要改别的地方。
还有一点非常重要:三者的执行模型都是逐行处理,也就是流式处理。这意味着不管文件有多大,哪怕几个GB,内存里永远只保留当前这一行,不会因为文件大而撑爆内存。相比之下,你用vim打开一个1GB的文件可能直接卡死,但用grep去扫这个大文件,轻松得很。
所以,对于运维工程师、后端开发、数据分析师来说,这三件套不只是"会几个命令"的问题,它直接决定你处理日常任务的效率上限。说它们是Shell脚本的基石,一点都不夸张。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. grep:文本过滤的精准打击
2.1 从基础匹配到扩展正则的进阶路径
grep看起来简单,实际上常用的几个参数如果拿捏好了,威力完全不一样。先从最基础的开始,看看这段实操中高频出现的组合。
bash复制# 基础字符串匹配,找日志中的ERROR行
grep "ERROR" app.log
# 忽略大小写匹配(日志里可能混着Error、error、ERROR)
grep -i "error" app.log
# 反向匹配:排除含DEBUG的行
grep -v "DEBUG" app.log
# 扩展正则:匹配ERROR、FATAL、WARN三种级别(注意-E参数)
grep -E "ERROR|FATAL|WARN" app.log
# 只输出匹配到的部分,而不是整行(配合-o,是字段提取的利器)
grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" app.log
我特别想强调两个参数:-E和-v。-E表示使用扩展正则,如果不加它,你写(a|b)这种分组语法就是不识别,这是新手最容易踩的坑。-v是反向选择,在日志量巨大的时候,先排除掉垃圾信息再处理,往往比正向选目标更快。
2.2 实战场景:日志和进程排查的常用姿势
我排查线上问题时的常用姿势,基本跑不出下面几个组合:
bash复制# 查一个接口的报错日志,并带上前后5行上下文
grep -B 5 -A 5 "支付接口超时" app.log
# 递归搜索代码目录,只在指定的文件类型里查(超实用)
grep -rn --include="*.py" --include="*.go" "TODO" ./project/
# 排除编译产物和第三方库
grep -rn --exclude-dir=node_modules --exclude="*.min.js" "console.log" .
# 统计错误出现的次数
grep -c "FATAL" app.log
# 过滤进程时排除grep自己(老手也会犯的错误)
ps aux | grep "[j]ava"
这里有个经验之谈:先grep -c看数量,再grep -B/-A看上下文。一上来就铺天盖地输出,人眼根本看不过来。你先搞清楚量有多大,是零星报错还是刷屏级别的故障,再决定下一步怎么处理。
2.3 grep高频踩坑与破解方法
- 忘记加-E导致正则失效:
grep "(ERROR|FATAL)"默认是基本正则,括号会被当普通字符。我建议默认就记成grep -E,统一成扩展正则,少踩很多坑。 - 匹配到二进制文件刷屏:grep遇到带特殊字符的二进制文件会输出"Binary file ... matches",可以用
-a参数强制把二进制当文本处理,或者直接用-I忽略二进制文件。 - 管道中长时间无输出:如果你用
tail -f app.log | grep "ERROR",grep默认是全缓冲,日志输出会积攒到缓冲区满了才显示。这时候必须加--line-buffered参数,让grep逐行刷新输出。
3. sed:流式编辑器的思维转变
3.1 从"打开文件编辑"到"处理数据流"的心智模型
sed最颠覆直觉的地方是:它根本不会"打开"文件,而是以流的方式从输入源读一行、处理一行、输出一行。理解不了这个模型,你用sed就永远只是在背命令。
我们假装自己是sed,想象它读入一行内容后,发生了这些事:
- 把这一行放进模式空间(pattern space);
- 执行你给的命令(比如替换、删除);
- 把模式空间处理后的结果输出到屏幕或文件;
- 清空模式空间,读取下一行,重复整个过程。
知道了这个模型,很多怪现象就有了解释。比如sed 's/old/new/'只替换每行第一个匹配,因为每次处理都是一行,默认动作就是对这行执行一次命令。想替换全部,必须加g标志告诉它"这一行里有多少个都给我换掉"。
再比如,你让sed删除第10行到第20行,它并不会一次读入全部内容,它只是读每一行时判断一下"当前行号在不在10到20之间",在就丢弃,不在就输出。所以就算文件有10个GB,它也吃得消。这也是为什么我总跟人说,别把sed当"编辑器",要把它当"生产线"——它是逐个零件(行)做加工,不是把所有零件堆在桌上一起改。
3.2 地址定界与常用命令
sed命令的基本结构是:[地址] + [命令]。地址可以是行号、行号范围或者正则表达式。搞懂了地址,sed就学会了一大半。
bash复制# 只替换第20行的old为new
sed '20s/old/new/g' file.txt
# 替换第10行到第20行
sed '10,20s/old/new/g' file.txt
# 从匹配到BEGIN的行开始,到匹配到END的行结束,每行替换
sed '/BEGIN/,/END/s/old/new/g' file.txt
# 删除所有空行
sed '/^$/d' file.txt
# 打印第10到20行(必须加-n,否则会每个print两次,一次是流本身的输出,一次是p命令主动print)
sed -n '10,20p' file.txt
# 在匹配到的行后面追加一行内容
sed '/listen 80;/a\\ listen 8080;' nginx.conf
这里必须提醒一句:p命令配合-n使用才是"只打印我想要的";不写-n,sed会把每行都打印一遍,再把匹配行额外打印一遍,输出会重复。这是我见过新手搞不清、老手偶尔也会脑子短路的点。
3.3 实战场景:批量改配置与反向引用
批量修改配置文件,是sed最爽的场景。来看两个我日常高频使用的例子。
第一个是修改SSH配置:
bash复制# 把被注释的PermitRootLogin行解注并改成yes(实际生产请按安全规范操作)
sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
第二个是反向引用,这个技巧强烈建议掌握。你想把"2024-05-01"这种日期格式改成"2024/05/01",用基础写法要写好几条规则,但用正则分组加反向引用,一条搞定:
bash复制sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\1\/\2\/\3/g' date.txt
\1、\2、\3分别对应前面正则里三个括号组捕获到的内容。你把它理解为"把第几组括号匹配到的内容原样搬过来",非常直观。
3.4 原地修改的注意事项
sed -i是"原地保存"的开关,不加它的话,sed所有结果只会打到屏幕,文件本身没变。这其实是个保护机制——把命令输出检查一遍,确认无误再加-i,是个特别好的习惯。
bash复制# 先输出到屏幕,检查无误后再写回
sed 's/old/new/g' config.txt
sed -i 's/old/new/g' config.txt
# 保险起见,先备份再改(生成config.txt.bak)
sed -i.bak 's/old/new/g' config.txt
macOS自带的BSD sed和Linux上的GNU sed在-i上有个坑:BSD的-i后面必须跟一个后缀,sed -i 's/old/new/' file这种写法在macOS上会直接报错。跨平台写脚本时,建议统一写成sed -i.bak '...' file,两边都兼容,还能留下备份回头核对。
4. awk:结构化文本的分析利器
4.1 行-列模型与内置变量的精妙之处
如果说sed的思维核心是"流",那awk的思维核心就是**"行 + 列"**。awk默认把一行文本按空白字符(空格、Tab)拆成多个字段,$1表示第一列,$2表示第二列,$0表示整行。
但awk真正的原力在于它自带一堆内置变量,你不需要自己写代码去数行号、数列数,这些信息它都帮你算好了:
| 内置变量 | 含义 | 典型用途 |
|---|---|---|
NR |
当前处理的是第几行(全局行号) | 跳过表头:NR>1 |
NF |
当前行的字段数(列数) | 判断一行是否完整 |
FS |
输入字段分隔符,默认是空格 | 处理/etc/passwd时设为: |
OFS |
输出字段分隔符,默认是空格 | 控制输出格式 |
$0 |
当前整行 | 原样输出或整行处理 |
$1~$NF |
各列的值 | 提取字段 |
awk的基本结构是"模式 + 动作":awk 'pattern {action}'。模式匹配成立的行才执行花括号里的动作。这个模式和sed很像,但awk的动作能做更复杂的编程。
举个例子,你想快速看看机器上登录用户和他们的家目录:
bash复制# -F 指定分隔符为冒号
awk -F: '{print $1, $6}' /etc/passwd
4.2 实战场景:统计、求和与格式化输出
统计求和是最典型的awk需求。比如计算一组数字的总和和平均值:
bash复制# 假设data.txt里每行一个数字
awk '{sum += $1; count++} END {print "总和:", sum, "平均值:", sum/count}' data.txt
END关键字的意思是"所有行都处理完后,执行一次这里的动作"。这个设计非常顺手,相当于在流水线结束的地方做一个最终汇总。
格式化输出是awk的另一大优势。printf你肯定在C语言里见过,awk里用法几乎一样,但注意format字符串写起来需要带上括号:
bash复制# 左对齐输出:%-20s表示左对齐占20个字符宽度,%8.2f表示保留两位小数的数字
awk '{printf "%-20s %8.2f\n", $1, $2}' data.txt
这种格式化的意义在于:当你要把结果贴到表格工具或者发到群里的时候,对齐的文本一眼就能看懂。不格式化的话,乱七八糟的缩进会让人怀疑你根本不专业。
4.3 进阶:数组、循环与条件过滤
awk里也有关联数组,威力极大。统计Nginx日志里每个IP出现多少次,是经典的awk数组用法:
bash复制awk '{count[$1]++} END {for (ip in count) print ip, count[ip]}' access.log | sort -rn -k2 | head -10
这段代码的解释:count[$1]++表示用IP作为数组的下标,每遇到一次就加1。处理完所有行之后,遍历这个数组把所有IP和次数打出来,再用sort按次数倒序排,取前10。
条件过滤也一样顺手,比如找出第三列大于100的行:
bash复制awk '$3 > 100 {print $0}' result.txt
如果你之前只把这几个命令当"搜索工具"用,我建议专门花一两个小时,把awk的数组、循环、格式化这三个点练一遍。它们看起来有点编程的味道,但恰恰是这种感觉,才让awk甩开grep和sed一个段位。
5. 三件套的组合实战与高频问题排查
5.1 一条管道链搞定复杂任务
工具单用是零件,组合起来才是机器。很多时候,一个看似复杂的任务,拆解后就是一条有先后顺序的管道链。我拿一个Nginx访问日志分析的例子串一下。
假设access.log的格式是标准的combined格式,其中$1是客户端IP,$7是请求的URL,$NF是响应时间(在指定日志格式的情况下)。现在需要找出响应时间超过3秒的请求,并按时间降序输出URL和耗时,取前20条:
bash复制awk '$NF > 3 {print $NF, $7}' access.log | sort -rn | head -20
再比如,你想知道今天哪些IP产生过5xx错误:
bash复制# 先grep出状态码为5xx的行,再用awk取IP字段,再去重统计
grep -E '" 5[0-9][0-9] ' access.log | awk '{print $1}' | sort | uniq -c | sort -rn
sort | uniq -c | sort -rn这串"去重排序三板斧"是Shell里面的经典套路:先排序让相同行相邻,再用uniq -c压缩连续相同行并加计数,最后按计数倒序排。记住这个组合,很多"统计频率"类需求都能立刻套用。
再举个实用的开发场景。假设你项目里的配置文件分散在几十个文件中,要把所有http://替换成https://,但要排除localhost相关的行,一条管道就能扫完并确认改动:
bash复制grep -rl "http://" --include="*.conf" . | xargs sed -i 's#http://#https://#g'
这里有个小技巧:替换命令的分隔符可以换成#,这样URL里的斜杠/就不需要转义了,读写都清爽很多。用grep -rl先找出包含目标串的文件列表,再交给xargs逐文件执行sed替换,这是"先定位后修改"的标准做法。
5.2 常见问题与排查技巧速查表
写脚本和敲命令这么多年,总结了一张高频问题表,遇到直接查就行。
| 现象 | 原因 | 解决方法 |
|---|---|---|
| grep匹配"ERROR"时把"ERRORED"也匹配出来 | 没有做单词边界限制 | 用grep -w "ERROR"限定整词匹配 |
| sed替换不生效 | 忘记-E导致正则写法不对,或没有g导致每行只替换第一处 |
统一用sed -E,替换全文记得加g |
| awk输出的数字精度不对 | awk浮点计算默认精度有限 | 用printf "%.2f"限制小数位 |
| 变量在单引号里传不进awk | Shell单引号内的$变量不展开 |
把awk脚本写成双引号包裹,或通过-v参数传入变量,例如awk -v val="$var" '{print $1+val}' |
| sort按数值排却按字典序排了 | sort默认按字符串排序,10会排在2前面 |
数值排序加-n |
| grep匹配中文或特殊字符乱码 | 文件编码不是UTF-8 | 先用file命令看编码,必要时用iconv转码后再grep |
| 多个grep管道匹配太慢 | 管道太多导致每个工具都全量读一遍文件 | 能在一个awk里写完的条件合并到一个awk,减少数据往返 |
5.3 我的几条独家心得
- 先打印,再写回。sed和awk能原地改文件,但你最好在命令行先不加
-i跑一遍,用眼睛确认输出符合预期,再补上-i正式执行。这条救了我太多次了。 - 正则表达式先在线验证再上生产。你可以本地用
echo "待匹配文本" | grep -E "你的正则"这种做法快速验证,避免面对几十万行日志时才发现正则写错。 - 能组合就不写循环。Shell脚本里教条地用for循环逐行处理文件,是最慢也最难维护的做法。grep/sed/awk的管道方案既高效又简洁。
- 注意命令的跨平台差异。同样的sed语法,在Linux和macOS上可能表现不一致。如果你的脚本要在多个环境跑,尽量用
POSIX兼容写法,并提前在两台机器上测试。 - 日志字段预处理要在源头规范化。如果你能控制程序输出的日志格式,务必用固定的分隔符和字段顺序。这样awk处理时就非常舒服,不用为各种奇葩格式写一堆兼容逻辑。我见过太多研发同学日志格式五花八门,后来出问题只能面对一堆完全没法用awk解析的文本,搞得人都快疯了。
6. 从命令到脚本:让三件套真正嵌入你的工作流
6.1 单行命令演化成Shell脚本的正确步骤
很多人学完grep、sed、awk,单条命令用得挺溜,但一到写脚本就不知道怎么组织。我的建议是,写脚本之前先在命令行把核心管道链跑通,再包一层参数和判断。
举个例子,你想写一个脚本,统计指定日志文件中某个状态的请求量。先在命令行验证:
bash复制awk -v status=200 '$9 == status {count++} END {print count}' access.log
验证通过,再把它扩成一个可复用脚本:
bash复制#!/usr/bin/env bash
log_file="$1"
status_code="$2"
awk -v status="$status_code" '$9 == status {count++} END {print count}' "$log_file"
这样脚本的骨架是非常清晰的:参数接收、核心命令、输出结果。不要一上来就套一堆if else和for循环,那只会让事情变复杂。
6.2 变量传递与引号处理的底层逻辑
在Shell里写awk和sed脚本,最容易出问题的就是变量传递和引号。这个真的得掰开揉碎讲清楚。
Shell的单引号里不展开任何变量,但awk脚本里又经常需要用到Shell变量。标准做法是用-v把Shell变量传进去:
bash复制threshold=3
awk -v t="$threshold" '$NF > t {print}' access.log
这样awk内部直接用变量t,不需要关心Shell变量的引号问题。如果硬要用双引号拼写awk脚本,遇到脚本里有$1这种awk字段符号时,会被Shell当成位置参数展开,然后你就发现awk打印出来的全是空。能用-v就绝不用双引号拼接,这条规则能让你少掉一半头发。
6.3 日志分析、巡检、报警:三件套能做的三大件
文本三件套最常见的落地场景有三类,我把它们写到脚本里,Linus之流的日常巡检完全够用。
日志分析:定期统计报错频率、异常IP、慢请求TopN。这类脚本可以配合cron定时跑,结果写入文件或者推送到企业微信机器人。
巡检报告:检查磁盘使用率、服务进程存活状态、关键端口监听情况,然后格式化输出一份报告。
bash复制awk 'NR>1 {if ($5 > 80) print $NF, $5}' <(df -h | awk 'NR>1')
进程守护:如果服务进程不在了就自动拉起。这种脚本里会用到pgrep等命令,但本质思想一样:先查、再判断、最后执行。
这三个场景通用性很强,大家完全可以拿我上面给的片段当模板去扩展。别想着一口气写出个完美脚本,先跑通,再加健壮性,这个思路我自己验证了很多年,确实比"憋大招"要靠谱得多。
我在实际使用中最深的体会有两个:一是管道思维真的需要刻意练习,刚上手时你可以把每一步拆开看中间输出,熟练之后就能一条命令直接写到底;二是能交给grep、sed、awk做的事,就不要写Python或者Java脚本,系统自带、免依赖、性能快到飞起的这三位老兄弟,真的是用一次就回不去了。希望这篇东西能帮你把文本处理这块的效率提上来,少踩几个我当年踩过的坑。
