Shell 三剑客:grep、sed、awk——找、改、算,一个都不能少
开头
学 Linux 到这会儿,文件操作、进程管理、服务控制这些我多少都能上手了。但每次看网上教程里那种"一条命令处理日志"的写法——cat xxx.log | grep 报错 | awk '{print $2}' | sort | uniq -c——我就头皮发麻,感觉像在看外星文。
这周老师正式讲了传说中的三剑客:grep、sed、awk。名字听着挺唬人,其实三个都是处理文本的工具。但一开始我完全分不清它们谁是谁——不都是"对文本做点什么"吗?为啥要搞三个?
后来老师一句话点醒了我:
grep 负责"找",sed 负责"改",awk 负责"取列和算"。
文本处理无非就这三件事。这篇就把我学三剑客的过程、踩的坑、还有连招玩法都记下来。
环境还是那台 CentOS 7 虚拟机,VMware 里跑的。前面被配置文件搞崩过一次,这次学三剑客之前先打了个快照。
三剑客到底各管哪摊事?
先建个整体认知,后面就不容易混:
| grep | 源自 ed 编辑器的 g/re/p(全局搜索正则并打印) | 找行 | 把"符合条件"的行过滤出来 |
| sed | Stream Editor,流式编辑器 | 改 | 对文本做替换、删除、插入,非交互 |
| awk | 三个作者 Aho、Weinberger、Kernighan 的首字母 | 取列/算 | 按列拆分,取字段、做统计 |
拿同一段数据试试水,三兄弟各出手:
# 假设 /etc/passwd 里有一行:
# root:x:0:0:root:/root:/bin/bash
grep "root" /etc/passwd # 把包含 root 的整行捞出来
sed 's/bash/zsh/' /etc/passwd # 把每行里的 bash 替换成 zsh
awk -F: '{print $1}' /etc/passwd # 按冒号拆开,只取第一个字段(用户名)
我当时的理解是:grep 管"行",awk 管"列",sed 管"行里面的内容"。虽然不严谨,但做题时够用。
真正厉害的用法是把它们串起来,各干一段:
日志文件 → grep 筛出要的行 → sed 把格式改一改 → awk 取出字段 → sort / uniq 统计
1. grep——把我要的行捞出来
基本用法
grep 最朴素的用法,就是在一个文件里搜关键词:
grep "root" /etc/passwd
# root:x:0:0:root:/root:/bin/bash
# operator:x:11:0:operator:/root:/sbin/nologin
# ……
grep "ERROR" /var/log/messages # 在日志里找报错
没给文件的时候,grep 会从标准输入读,所以配合管道是它的主战场:
ps aux | grep sshd # 进程列表里找 sshd
history | grep "vim" # 历史命令里翻 vim 相关的
常用参数
| -i | 忽略大小写 | grep -i error log(能匹配到 ERROR/Error) |
| -v | 反向匹配,输出"不含"关键词的行 | grep -v "^#" sshd_config(去掉注释行) |
| -n | 显示行号 | grep -n "server" config |
| -c | 只数匹配的行数 | grep -c "error" log |
| -r | 递归搜整个目录 | grep -r "port" /etc/nginx/ |
| -o | 只输出匹配的部分,不输出整行 | echo "abc123def456" | grep -oE "[0-9]+" |
| -w | 全词匹配 | grep -w "is" file(不会匹配到 this) |
| -E | 用扩展正则 | grep -E "(error|warning)" log |
正则入门——grep 是最好的练手工具
学 grep 顺便把正则入门了。最常用的几个元字符:
| . | 任意单个字符 | grep "a.c" 能匹配 abc、a1c |
| * | 前一个字符重复 0 次或多次 | grep "ab*c" 能匹配 ac、abc、abbc |
| ^ | 行首 | grep "^root" /etc/passwd |
| $ | 行尾 | grep "bash$" /etc/passwd |
| [abc] | 字符集合 | grep "gr[ae]y" 能匹配 gray / grey |
踩坑:基本正则和扩展正则。 我练 grep "(error|warning)" log 的时候,啥也搜不到。后来才知道 grep 默认用的是基本正则,里面 ( )、+、?、| 这些"高级"符号都被当成普通字符,必须加 \\ 转义,或者直接加 -E 用扩展正则:
grep "\\(error\\|warning\\)" log # 基本正则要加反斜杠
grep -E "(error|warning)" log # 加 -E 就不用转义了(推荐)
egrep "(error|warning)" log # egrep 就是 grep -E 的别名
我至今记得 \\(…\\|…\\) 这种写法,太反人类了,现在一律 grep -E。
grep 的坑
坑1:grep 匹配到了它自己。
ps aux | grep sshd
# 输出里会混进一行:
# root 12345 0.0 0.0 11292 4320 pts/0 S+ 08:30 0:00 grep –color=auto sshd
因为 ps aux | grep sshd 这个命令本身也包含 “sshd” 几个字,grep 就把自己那行也捞出来了。解决办法是再加个 -v grep:
ps aux | grep sshd | grep -v grep
我第一次看到 grep 自己出现在结果里,还以为系统坏了,琢磨了半天。
坑2:大小写。 日志里搜 error,结果日志里写的是 ERROR,啥也没搜到。搜之前先在脑子里过一遍要不要 -i。
坑3:搜目录直接报错。 grep "abc" /etc/ 会报 grep: /etc/: Is a directory。要加 -r 才能递归进目录。
2. sed——不打开文件也能改文件
sed 全名叫 Stream Editor(流式编辑器)。我第一次理解它是这样的:vim 是"打开文件,我手动改",sed 是"不打开文件,一条命令自动改"。适合批量处理,尤其是改配置文件。
替换:s/旧/新/
最常用的操作是替换,语法是 s/旧内容/新内容/:
sed 's/root/ROOT/' /etc/passwd # 把每行第一个 root 换成 ROOT
sed 's/root/ROOT/g' /etc/passwd # 加 g,把每行所有 root 都换掉
sed 's/root/ROOT/2' /etc/passwd # 数字 2,只替换每行第 2 个 root
坑:不加 g 只替换每行第一个。 我有次想把配置里所有的 localhost 都改成服务器 IP,用了 sed 's/localhost/192.168.1.10/',结果每行只换了第一个,后面没换到的全漏了。后来才发现要加 g(global,全局)才会替换行内所有匹配。
按行号 / 范围操作
sed -n '2p' file # 只打印第 2 行(-n 是"别自动打印",p 是打印)
sed -n '2,5p' file # 打印第 2 到 5 行
sed '3d' file # 删除第 3 行
sed '5,10d' file # 删除第 5 到 10 行
sed '/^#/d' file # 删除所有以 # 开头的行(删注释)
sed 's/^#//' file # 把每行开头的 # 去掉(取消注释)
-n 和 p 组合是 sed 里的经典搭配,意思是"只打印我想看的行"。这个我一开始老是忘,不加 -n 的话,整个文件会被打一遍,然后匹配行再打一遍,输出老长老长。
插入和追加
sed '2i hello' file # 在第 2 行上面插入一行 hello(i = insert)
sed '2a world' file # 在第 2 行下面追加一行 world(a = append)
sed '/root/a this is root' file # 在包含 root 的行后面追加一行
原地修改:-i,血泪教训
前面所有操作,不写 -i 都只是打印到屏幕上,文件本身没变。要真的改文件,必须加 -i:
sed 's/old/new/g' file # 只显示结果,文件没动
sed -i 's/old/new/g' file # 直接改文件(危险,改完不可逆)
sed -i.bak 's/old/new/g' file # 改文件,同时把原文件备份成 file.bak
我的翻车经历:第一次用 sed 改配置文件,我以为像 vim 一样改完就保存了,结果敲完 sed 's/8080/9090/' nginx.conf,一看文件还是 8080,愣了半天。补上 -i 才算改掉。后来更惨的一次,-i 改完发现改错了,又没备份,只能靠快照回滚。
所以现在养成习惯:用 -i 之前先 -i.bak 备份一份,或者先不加 -i 跑一遍看结果对不对,确认没问题再上 -i。
还有一个坑:路径里带斜杠。替换路径时,比如要把 /usr/local 换成 /opt,直接写 sed 's//usr/local//opt/' 就全乱了,因为 / 既是定界符又是内容。解决办法是换个定界符,比如用 |:
sed 's|/usr/local|/opt|g' file
这招后来帮我省了很多事。
3. awk——把文本当表格,按列处理
awk 是三个里最"重"的一个,它其实是一门小语言。但在学生阶段,我们主要用它做两件事:按列取字段 和 按条件算数。
名字来源挺有意思,是三个作者的首字母:Aho、Weinberger、Kernighan。
按列取字段
awk 默认按"连续空白"(空格/制表符)把每行拆成字段,$1 是第一个字段、$2 是第二个……$0 是整行:
# 假设 ps aux 的输出:USER PID %CPU %MEM …
ps aux | awk '{print $1}' # 只打印第一列(用户名)
ps aux | awk '{print $2}' # 只打印第二列(PID)
ps aux | awk '{print $1, $2}' # 打印第一、二列
$NF 表示最后一个字段(NF = Number of Fields,字段个数),所以 $NF 就是"第 NF 个字段"。
awk '{print $NF}' file # 打印每行最后一个字段
坑:$NF 和 NF 分不清。 我第一次看 awk '{print $NF}' 想了半天。$NF 是"取最后一个字段的内容",而裸的 NF 是"这一行总共有几个字段"。一个是内容,一个是数字,做题的时候经常在这翻车。
指定分隔符:-F
处理 /etc/passwd 这种用冒号分隔的文件,默认按空格拆就废了,要用 -F 指定:
awk -F: '{print $1}' /etc/passwd # 打印所有用户名
awk -F: '{print $1, $7}' /etc/passwd # 用户名 + 登录 shell
awk -F: '{print NF}' /etc/passwd # 每行有几个字段(passwd 是 7 个)
内置变量 NR 和 NF
- NR = Number of Record,处理到第几行了(行号)
- NF = Number of Field,当前行的字段个数
awk -F: 'NR==1 {print $0}' /etc/passwd # 打印第一行
awk '{print NR, $1}' file # 给每行加上行号
条件筛选和计算
awk 后面可以跟条件,条件成立才执行 {} 里的动作:
awk -F: '$3 >= 1000 {print $1}' /etc/passwd # UID 大于等于 1000 的用户
awk '$3 > 50 {print $1, $3}' stats.txt # 第三列大于 50 的行
awk 'NR > 1 {sum += $5} END {print sum}' file # 从第 2 行开始累加第 5 列
BEGIN 和 END 是两个特殊块:BEGIN 在读文件前执行(常用来打表头),END 在处理完所有行后执行(常用来输出汇总):
awk -F: 'BEGIN {print "用户名\\tShell"} {print $1"\\t"$7} END {print "统计完毕"}' /etc/passwd
awk 的一个完整实战
老师留的作业:统计一个日志文件里每个 IP 出现了多少次(类似看谁在刷服务器)。
# 假设日志每行第一个字段是 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
拆解一下:
- awk '{print $1}' — 把每行的 IP 抠出来
- sort — 排序(uniq 只能统计相邻的重复项,必须先 sort)
- uniq -c — 统计每个 IP 出现次数
- sort -rn — 按次数从大到小排
- head -10 — 取前 10 名
我第一次写的时候漏了 sort,结果 uniq -c 统计出来全是 1,以为日志有问题。后来才知道 uniq 只合并相邻的行,不排序就统计等于白统计。
awk 的坑
坑1:单引号不能少。 awk 的程序部分要包在单引号里,awk '{print $1}'。我试过用双引号 awk "{print $1}",结果 $1 被 shell 先解析成空字符串了,打印出来全是空行。awk 里的 $ 要交给 awk,别让 shell 吃掉。
坑2:默认分隔符是连续空白。 处理 root:x:0:0 这种没有空格的文本,不加 -F: 只会得到一整块,字段根本没切开。
坑3:print 里逗号是有意义的。 print $1, $2 两个字段之间会有一个空格(默认分隔符),而 print $1 $2 是直接拼在一起。我第一次没注意,输出挤成一团。
三剑客连招:一条命令搞定一件事
单独用每个都能用,但真正的威力是把它们串起来。分享两个我练过的完整案例:
需求:找系统里所有能登录的用户,看看他们的 shell 正不正常。
# 1. grep 过滤掉不能登录的(shell 是 nologin 的)
# 2. awk 取用户名和 shell
grep -v "/sbin/nologin" /etc/passwd | awk -F: '{print $1, $7}'
需求:统计当前系统内存占用最高的几个进程。
# ps aux 的列:USER PID %CPU %MEM VSZ RSS … COMMAND
# sort 按第 4 列(%MEM)从大到小排,再 awk 取 PID、内存、命令
ps aux | sort -rk4 | head -5 | awk '{print $2, $4, $11}'
需求:看看配置文件里注释写了多少行。
grep "^#" /etc/ssh/sshd_config | wc -l
# 或者用 awk 数
awk '/^#/ {count++} END {print count}' /etc/ssh/sshd_config
练连招的最大感受是:每条命令都只做一件事,靠管道把它们拼起来。这跟我之前在 [[7.22 创建、查找和编辑文本文件]] 学的管道是同一件事——一个命令的输出,是下一个命令的输入。
我踩过的坑合集(考前必看)
| grep 匹配到自己 | ps aux | grep sshd 结果里混进 grep 自己 | 加 grep -v grep,或直接 pgrep sshd |
| 基本正则 / 扩展正则 | grep "(a|b)" 不生效 | 用 grep -E 或 egrep |
| sed 忘记 -i | 跑完 sed 's/x/y/' 文件没变 | 确认无误后加 -i,最好 -i.bak 备份 |
| sed 替换路径斜杠混乱 | 替换带 / 的路径写错 | 用 ` |
| sed 不加 g | 每行只替换第一个匹配 | 全替换要加 g |
| awk 用了双引号 | $1 被 shell 吃掉,输出全是空 | awk 程序必须用单引号包 |
| awk 忘了 -F | 处理 passwd 只能取到一整块 | 冒号分隔用 -F: |
| uniq 前没 sort | 统计结果全是 1 | uniq -c 前先 sort |
| $NF 和 NF 混淆 | 想取最后一列却写成 {print NF} | 取内容用 $NF,取数量用 NF |
速记清单
grep 管找行,sed 管改内容,awk 管取列/算
grep:-i 忽略大小写,-v 反向,-E 扩展正则,-o 只输出匹配
sed:s/旧/新/g 替换,-n 配 p 只打印,-i 才真正改文件
awk:默认按空白拆列,-F: 指定冒号分隔
awk:$1 取第 1 列,$NF 取最后一列,NR 是行号,NF 是列数
uniq -c 之前必须 sort
下一步打算
- 正则表达式还想再系统练一遍,grep、sed、awk 都离不开它
- 学学 awk 的 if、for 这些语法,它其实是门完整的编程语言
- 想试着写一个真正能用的日志分析小脚本,把三剑客 + 管道用熟
最后说两句
学完三剑客,最大的感受是,以前觉得"高深"的日志处理命令,其实就是把三个各司其职的小工具用管道拼起来:grep 找、sed 改、awk 取,各干各的,组合起来能干挺多事。
不过说实话,awk 我现在还是用得最生疏,它的语法长得跟 C 有点像,一复杂就犯晕,估计后面写脚本的时候得多练。sed 的 -i 我现在每次用之前都要确认一遍有没有备份。
还有个小事:三剑客配合正则的时候符号真的很多,^、$、.、*、[] 都是啥意思,我到现在偶尔还要翻 man grep。反正记不住就查呗,用多了自然就熟了。
写这篇的时候,虚拟机里 /var/log 的日志被我 grep / sed / awk 来回折腾了个遍,还好有快照,玩坏了随时回滚。
2026年8月 · 写于 shell 编程课后 · 虚拟机里 /var/log 的日志被我翻了个底朝天
网硕互联帮助中心





评论前必须登录!
注册