日常在 Linux 服务器上处理问题,grep、awk、sed 这三条命令是我使用频率最高的工具。查日志要筛行,看进程要定位,改配置又要批量替换,大部分工作靠这三个命令就能完成,很多时候甚至不需要打开文件编辑器。这篇就把我平时真正在用的姿势整理一遍,按“筛行、切列、改内容”的逻辑拆开讲,再配上几个实际排查场景,适合已经会敲基础命令、但还没把三者串起来用的人参考。
有人会把它们当成三个独立的命令去背参数,我的经验是反过来:先把它们分别擅长什么搞清楚,再考虑怎么组合。grep 关注的是“行”,awk 关注的是“列”,sed 关注的是“怎么改”,理解了这个分工,命令写起来就顺了。
1. grep 先搞清楚“搜什么”:正则、纯文本与高频排查参数
1.1 grep 是行过滤器,不是文件搜索器
很多刚开始用 Linux 的人会把 grep 当成“全文搜索工具”,其实它本质是一个逐行读取、逐行匹配的过滤器。它只对输入流里的每一行做判断,匹配成功的行会输出,不匹配的就被丢弃。输入流可以来自文件,也可以是上一个命令通过管道传过来的结果,这两个场景在日常工作中几乎一样多。
基本的匹配逻辑很简单:
bash复制grep "ERROR" app.log
grep -n "timeout" app.log
grep -c "ERROR" app.log
-n 会输出行号,定位问题的时候特别有用;-c 只输出匹配行数,适合快速判断某个关键字出现的量级。还有一个我经常用的 -v,作用是反向选择,把不匹配的行输出出来。比如看 nginx 配置时想去掉注释和空行,可以这样:
bash复制grep -v "^#" nginx.conf
这里 ^ 是正则里的行首锚点,^# 表示行首是井号的行。grep 默认是支持基础正则表达式的,所以这些符号有特殊含义。后面我会单独说这个点,因为这里藏着新手最容易踩的坑。
-i 忽略大小写,-o 只输出匹配到的部分而不是整行,这两个参数在实际用的时候也很顺手。比如搜进程的时候,你记得名字里有 service 但不确定大小写,直接 ps aux | grep -i service 就好。-o 则适合从一行日志里把某个编号全部抠出来,再配合 sort、uniq 做统计。
1.2 正则不是什么时候都该开:-F、-E、-P 到底怎么选
grep 支持三种正则风格,默认用的是基础正则(BRE),加了 -E 用扩展正则(ERE),加了 -P 用 PCRE。很多人在网上抄命令时会看到 grep -E,但不太清楚它和默认有什么区别。最直观的差异是:在基础正则里,|、+、?、() 这些符号都要加反斜杠才有特殊含义;在扩展正则里直接用就行。
举一个真实的例子。我想在日志里同时找 error 和 warn:
bash复制grep -E "error|warn" app.log
如果不用 -E,就得写成 grep "error\|warn" app.log,多写不少转义符。所以当你的匹配条件里出现“或”这种逻辑,直接用 -E 基本不会错。
比正则更容易被忽略的,是“根本不需要正则”的情况。比如你在配置文件里找一个 IP 地址 192.168.10.1,如果直接执行:
bash复制grep "192.168.10.1" config.ini
这里的点号在正则里表示“匹配任意一个字符”,也就是说 192x168y10z1 也会被匹配出来。虽然大多数时候配置里不会有这种巧合,但这不是一个严谨的写法。想要原样匹配纯文本,用 -F,它的意思是固定字符串(fixed string):
bash复制grep -F "192.168.10.1" config.ini
再比如搜带有 [INFO] 前缀的日志行,中括号在正则里也有特殊含义,不加转义甚至会直接报错。类似这种场景,我都建议先想清楚:我到底是在搜一个普通字符串,还是在搜一种文本模式?如果是前者,grep -F 最省心。
1.3 查进程、查端口、查硬件:高频排查命令的参数细节
服务器上最常见的 grep 场景就是和 ps、ss、lspci 配合。先说查进程,很多人会写:
bash复制ps aux | grep -i openclaw
这句的意思是把所有进程列出来,筛出命令行里包含 openclaw 的行。问题在于,grep 命令本身运行的时候,它的进程信息里也包含“grep”这几个字符,所以输出结果里经常会混进一条 grep 自己的进程。如果你是找到 PID 后准备去 kill,很可能把 grep 这个临时进程干掉,真正的目标进程却毫发无伤。
一种经典写法是用字符类技巧,把匹配模式写成一个“中间隔着方括号”的形式:
bash复制ps aux | grep "[o]penclaw"
原理很有意思:这个正则要求匹配的是字母 o 开头的 openclaw,而 grep 自己命令行里显示的是 [o]penclaw,方括号也参与匹配判断,结果反而不匹配它自己。如果你更习惯用 pgrep,那就更简单了:pgrep -f openclaw 直接返回 PID,不需要管道处理。
查端口类似。sudo ss -lntp | grep 8080 这个写法会列出所有监听(-l)的 TCP 端口(-t),并显示进程名(-p)。注意这里最好加 sudo,因为非 root 用户看别的进程,进程名和 PID 那一栏会被隐藏成空,命令能执行但拿不到关键信息。想更精确一点,可以用 ss 自带的过滤语法:
bash复制sudo ss -lntp 'sport = :8080'
至于显卡信息,lspci | grep -i nvidia 是查看 N 卡设备时我常用的命令。有些时候 lspci 输出的设备描述比较短,想确认驱动绑定关系,可以加 -k 参数看内核驱动模块名。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. awk 的列处理套路:从单纯的字段提取到统计汇总
2.1 awk 不是一个命令,而是一种“面向行的编程模型”
grep 只能帮你把符合条件的行筛出来,但如果我要的不是整行,而是这一行里某一列的内容,grep 就无能为力了。awk 干的正是这个活。
awk 的基本结构是 模式 { 动作 },它对每一行输入判断模式是否成立,成立就执行动作。不写模式时表示每一行都执行动作,不写动作时默认打印整行。比如我想把 /etc/passwd 里的用户名和 UID 打出来:
bash复制awk -F: '{print $1, $3}' /etc/passwd
awk 读取一行时,默认按空白字符(空格或 Tab)把行切分成若干字段,$1 表示第一个字段,$2 表示第二个,以此类推,$0 表示整行。-F: 的作用是把分隔符改成冒号,因为 passwd 文件用冒号分隔字段。如果你直接看输出,会发现用户名和 UID 之间用空格分开,这里的空格不是输入里带的,而是 print 默认拼接时加的逗号产生的输出分隔符。
理解 awk 的变量体系是写对命令的关键。下面几个是我最常用到的:
| 变量 | 含义 | 典型用法 |
|---|---|---|
| $0 | 当前整行 | awk '{print $0}' |
| $n | 第 n 个字段 | awk '{print $2}' |
| NF | 当前行的字段总数 | awk '{print NF}' |
| $NF | 最后一个字段的值 | awk '{print $NF}' |
| NR | 当前处理的是第几行 | awk 'NR==1 {print}' |
| FS | 输入字段分隔符 | awk -F: 等价于 awk 'BEGIN{FS=":"}' |
| OFS | 输出字段分隔符 | 控制 print $1, $2 之间的分隔符 |
这里特别容易混的是 NF 和 $NF。NF 是一个数字,代表“这一行被切成了多少列”;$NF 是把这个数字当作列号去取值,代表“这一行的最后一列”。比如 echo "a b c" | awk '{print NF, $NF}' 输出的是 3 c,NF 和 $NF 就差一个 $,含义完全不同。
2.2 提取、筛选、统计:最常用的三种写法
awk 可以直接在命令行里写条件判断。比如有一个成绩文件 score.txt,内容是:
text复制zhang 92
wang 78
li 88
想筛出分数大于 80 的人名,一行就够:
bash复制awk '$2 > 80 {print $1}' score.txt
这个写法看着简单,背后是关键点:awk 的模式部分可以直接写算术比较表达式,而不一定是正则。$2 > 80 的意思是当前行的第二列数值大于 80 时,执行后面的打印动作。在处理日志耗时、响应码这类数据时,这种数值筛选比正则匹配更高效,也更精准。
求和是另一个高频需求。比如一个文件里每行是一个数字,我想快速知道总和:
bash复制awk '{sum += $1} END {print sum}' data.txt
这里的 END 是一个特殊模式,表示所有行处理完之后执行一次。与之对应的还有 BEGIN,会在读文件之前执行,常用来初始化变量或者打印表头。这个“读文件时逐行累加、结束时统一输出”的模式,是 awk 做统计的核心思路。
去重也是日常里经常要用的。比如日志里重复出现某些请求 ID,我只想保留第一次看到的那一行:
bash复制awk '!seen[$1]++ {print}' app.log
这个写法初学者会觉得很抽象,拆开看就明白了:seen[$1] 是用第一列的内容做数组下标,每遇到一行就把这个下标对应的计数加一。第一次遇到某值时计数从 0 变成 1,!seen[$1] 在计数为 0 时成立,所以第一次出现的行会打印,后面重复出现的行计数已经大于 0,取反后条件不成立,就不再打印了。
2.3 多文件处理时,NR 和 FNR 的区别要注意
当 awk 同时读多个文件时,会出现一个容易看错的现象。比如:
bash复制awk '{print NR, $0}' a.txt b.txt
这里 NR 是全局行号,它会把 a.txt 和 b.txt 的行连起来编号,也就是说 b.txt 的第一行不是 1,而是接着 a.txt 的总行数继续往下排。如果你想在同时处理两个文件时知道“当前这个文件内部是第几行”,要用 FNR,它表示当前文件自身的行号。
bash复制awk 'FNR == 1 {print "文件头:", $0}' a.txt b.txt
这段命令会在两个文件各自的第一行都输出“文件头”。这就是用参数前先想清楚需求的例子:你到底需要的是全局位置,还是单个文件内的位置?这个细节在合并多个日志做检查时特别影响结果判断。
3. sed 的流式修改:替换语法、行定位与文件安全
3.1 替换命令的核心是“分隔符”和“修饰符”
sed 最常用的能力是替换。基本形式是 sed 's/旧内容/新内容/修饰符',其中 s 是 substitute 的缩写。
bash复制sed 's/foo/bar/' config.txt
这条命令会把每一行第一次出现的 foo 替换成 bar,然后输出到屏幕,文件本身没被修改。注意默认只替换每行第一次出现的位置,想替换整行所有匹配,需要加 g 修饰符:
bash复制sed 's/foo/bar/g' config.txt
很多人在这一步会犯迷糊:为什么我执行了 sed,文件内容一点没变?原因就是没有加 -i 参数。sed 默认只是一个流处理器,它把处理后的结果打印到标准输出,而不是写回文件。这是它的设计特点,也是它的安全优势——你可以先看输出结果对不对,再决定要不要真正写入。关于 -i 的细节我放到 3.3 节讲。
分隔符的选择也经常被忽略。当你要替换的内容里包含斜杠时,比如把 URL 里的 http:// 改成 https://,再用 / 做分隔符就要写成一堆转义:
bash复制sed 's/http:\/\//https:\/\//g' site.conf
这种写法容易看花眼。sed 允许你换分隔符,凡是 s 后面的第一个字符都会当作定界符。所以我一般遇到路径或 URL 会直接用 #:
bash复制sed 's#http://#https://#g' site.conf
整体清爽很多。常用的分隔符还有 | 和 @,核心原则就是选一个不会跟内容冲突的字符。
3.2 行号定位、删除和区间处理
sed 的另一个常用功能是按行号或模式定位,然后做删除、打印、插入等操作。处理大日志时我不想用 cat 把整个文件刷出来,只想看中间的一段,可以写:
bash复制sed -n '20,40p' app.log
-n 的作用是关闭默认输出,配合 p 表示只打印指定范围的行。没有 -n 的话,sed 会把所有行都输出一遍,其中 20 到 40 行会重复出现两次,完全不是想要的效果。
删除操作用 d。比如删除配置文件里的空行:
bash复制sed '/^$/d' config.txt
删除从第 5 行到第 10 行:
bash复制sed '5,10d' config.txt
删除以 # 开头的注释行:
bash复制sed '/^#/d' config.txt
这里的地址可以是行号、正则、或者是两者的组合。理解 sed 的“地址寻址”思路后,很多看起来复杂的编辑都能一行搞定。比如我想看 app.log 里从第一条 timeout 出现开始连续 5 行的内容:
bash复制sed -n '/timeout/,+5p' app.log
这种按模式定位区间的写法在排查异常时很方便。它跟 grep 的区别在于,grep 只是把匹配行给你,而 sed 可以给你“从匹配位置开始的上下文”,更接近编辑器里的范围选择。
3.3 写文件之前,先想清楚回滚方案
真正修改文件时用 -i,这个参数会让 sed 直接把处理结果写回原文件。我强烈建议习惯性地给 -i 加一个备份后缀,写成 -i.bak:
bash复制sed -i.bak 's#http://#https://#g' site.conf
执行后你会发现目录下多了一个 site.conf.bak,内容是修改前的原始版本。万一替换规则写错了,还能马上恢复。这个习惯帮我避免过不止一次线上配置改坏的问题。
另外,批量修改多个文件时,-i.bak 同样会为每个文件生成独立备份。用通配符让 sed 处理多个配置文件是常见操作:
bash复制sed -i.bak 's/old_server/new_server/g' /etc/nginx/conf.d/*.conf
这条命令会把 conf.d 目录下所有 .conf 文件里的 old_server 替换成 new_server。执行前我建议先不带 -i 跑一遍,把输出重定向到屏幕检查,确认替换结果没有意外,再补上 -i.bak 真正执行。先预览、再备份、后修改,这个三步流程能挡住绝大多数低级失误。
4. 三件套组合干活:追踪进程、归纳日志与批量改配置
4.1 场景一:为什么用 ps aux | grep 查脚本,PID 一直变
这个现象很多人遇到过。搜索热词里也有人专门问“ps aux | grep 脚本名,PID 一直变”。这里有两种可能性,排查思路完全不同。
第一种可能是你看到的其实是 grep 自己的进程。管道命令执行时,ps aux 把进程快照输出给 grep,而 grep 进程本身也在进程列表里。由于它只是瞬时运行,你在不同时刻执行,看到的 PID 会不一样。判断方法很简单:看输出行的命令列,如果那一行写的是 grep your_script,那这个 PID 就是 grep 自己,不是目标进程。想绕过它,可以用字符类技巧:
bash复制ps aux | grep "[y]our_script"
或者干脆用 pgrep:
bash复制pgrep -f your_script
第二种可能,是目标进程被守护程序自动拉起,导致进程本身频繁重启。用 pgrep 拿到当前 PID 后,再看它的父进程:
bash复制pid=$(pgrep -f your_script.py | head -1)
ps -o pid,ppid,lstart,cmd -p "$pid"
重点看 PPID 和 lstart。如果父进程是 systemd、supervisord 这类程序,而且启动时间非常短,说明脚本确实在被反复拉起。这时候就要去查服务配置里的重启策略、脚本本身的退出状态,而不是盯着 ps 的输出困惑。把“查进程”和“查父进程”分开做,能省下大量盲目排查时间。
4.2 场景二:一条管道梳理错误日志
假设日志文件 app.log 里每行是类似这样的结构:
text复制2025-06-01 10:20:11 ERROR user_id=12345 timeout
2025-06-01 10:21:03 WARN user_id=67890 slow
2025-06-01 10:22:47 ERROR user_id=12345 timeout
我想快速知道里面有多少条 ERROR,最直接的是 grep -c ERROR app.log。但我想进一步知道哪些用户 ID 出现错误最多,就需要三件套配合了:
bash复制grep "ERROR" app.log | awk '{print $4}' | sort | uniq -c | sort -rn
这条管道从左到右做的事情分别是:先用 grep 筛出包含 ERROR 的行,再用 awk 取出第四列(user_id=xxx 这部分),然后排序、去重计数、按数量倒序排列。uniq -c 会把每行出现的次数加在前面,sort -rn 按数字从大到小排。这是做“日志里哪个对象出现最多”问题的标准套路,比打开文件肉眼数快得多。
如果想统计错误类型占比,可以把条件写法换个位置,完全用 awk 完成判别和计数:
bash复制awk '/ERROR/ {count[$NF]++} END {for (k in count) print count[k], k}' app.log
这里 /ERROR/ 是模式,表示只处理包含 ERROR 的行;$NF 取这一行的最后一列 timeout;count[$NF]++ 对不同错误类型计数;END 里用循环输出结果。写完这条命令就能同时看出哪类错误出现的次数最多。实际使用时,先用 tail -n 20000 限定最近的两万行,再接入这条管道,能避免老日志干扰判断。
4.3 场景三:批量修改配置后的“自检”步骤
改配置的时候,三件套不仅能改,还能帮你确认改没改对。比如要把某目录下全部配置里的旧域名替换成新域名:
bash复制sed -i.bak 's/old.example.com/new.example.com/g' /etc/nginx/sites-enabled/*.conf
执行完不要急着走,先用 grep 反向检查一下还有没有遗漏的旧域名:
bash复制grep -R "old.example.com" /etc/nginx/sites-enabled/ || echo "没有残留旧域名"
-R 让 grep 递归读取目录下的所有文件,|| echo 利用命令退出码在“没有匹配到时”输出提示。如果还有残留,输出里会直接列出文件名和行,方便定位。这个“sed 改完 → grep 复查”的组合,是我每次批量操作后的固定动作。
这里插一句,批量修改前一定要先确认通配符范围没有把不该改的文件卷进来。因为 sed 对匹配文件不会有任何提醒,直接把规则套上去。范围越精确,风险越低。
5. 我常踩的几个语法坑和对应检查方法
5.1 grep 的元字符、sed 的 i 参数、awk 的字段取值
把这些坑集中整理成一张表,是我自己遇到问题时会拿出来对一遍的清单:
| 现象 | 原因 | 正确处理方式 |
|---|---|---|
| 搜 IP 或路径时结果里多了奇怪的行 | 点号等元字符被当成正则 | 用 grep -F 做纯文本匹配 |
| ps aux | grep 出来的 PID 是会变的“假进程” | grep 自身也被匹配 |
| 执行 sed 后文件没变化 | 忘了加 -i | 先不带 -i 预览,确认后加 -i.bak |
| sed 替换内容含斜杠,写出来全是反斜杠 | 分隔符与内容冲突 | 换 # 或 |
| awk 里想取最后一列,写成了 NF | NF 是字段数不是值 | 用 $NF 取最后一个字段值 |
| awk 输出多列时黏在一起难以分辨 | 没理解 OFS 配置 | 用 print $1, $2 逗号会自动补空格,或用 OFS=',' |
5.2 长命令拆解的小习惯
我在新手阶段最容易犯的错误,是抄一条很长的管道命令执行后发现结果不对,但不知道错在哪一段。后来养成了一个习惯:长命令拆开跑。先单独跑 grep 筛行,看筛出的行符不符合预期;再把这部分接上 awk,输出列确认位置;最后再加 sort、uniq、sed 这些后续处理。
另外还要注意 shell 引号的问题。管道和重定向是 bash 解析的,而正则表达式、awk 动作里的 $ 符号如果放在双引号里,会被 shell 当成变量拿去展开。举个例子,awk "{print $1}" 里的 $1 在双引号里会被 shell 变成空值或者第一个位置参数,最后输出的结果完全不是你想要的。所以awk 的动作和复杂的正则建议一律用单引号包裹,让它原样传给命令本身。
5.3 面对问题时先判断该用哪个工具,再想参数
最后说一个组合使用的整体思路。很多人的困惑不是命令记不住,而是遇到问题时不知道用 grep、awk、sed 里的哪一个。我的判断顺序是:
- 只是想看哪些行匹配、统计匹配行数,用 grep;
- 匹配到的行需要进一步取列、做数值比较、累加或者去重,用 awk;
- 需要在匹配结果基础上做删除、替换、插入,或者直接改一批文件,用 sed;
- 多个处理叠加时,按“grep 先筛、awk 再切、sed 最后改”的次序把它们用管道连起来。
比如排查一次接口变慢的问题,我会先 grep 定位到超时的日志行,再用 awk 取出耗时字段和请求 ID,然后可能需要 sed 去替换掉行尾多余字符统一格式,最后排序看最慢的几条。整个过程里工具之间是接力关系,每一步都在为下一步制造更干净的输入。平时遇到的绝大多数文本处理问题,都能被这套思维拆解掉。
写命令时多留一个心眼,把每一步要处理的“数据形态”想清楚,比如这一阶段输出的是整行、是某一列、还是修改后的文本,就不容易写着写着把自己绕晕了。我在实践中最大的体会是:这三条命令本身不难,难的是养成“用管道把问题拆小”的习惯。一旦习惯养成了,服务器上很多原本要写脚本才能做的事,其实一条命令就够。
