云计算百科
云计算领域专业知识百科平台

Shell 三剑客:grep、sed、awk——找、改、算,一个都不能少

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 的日志被我翻了个底朝天

赞(0)
未经允许不得转载:网硕互联帮助中心 » Shell 三剑客:grep、sed、awk——找、改、算,一个都不能少
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!