RHCSE(红帽认证系统工程师方向)的Shell实战系列走到第六篇,终于轮到 grep 了。按照前面的节奏,每篇只把一个点吃透,这次也不例外。先说定位:前面几讲学的都是单个命令,从这一篇开始,你才算真正进入 Linux 命令行最爽的阶段——“管道 + 过滤”。而 grep 就是过滤文本的第一主力。日常看日志、查端口、找进程、写脚本做数据清洗,几乎处处都有它的影子。这篇文章我会把 grep 的常用选项、正则规则、Shell 脚本联动方式和典型坑一次性讲透,适合正在备考 RHCSE、刚开始接触 shell 脚本的朋友,也适合那些已经用了很久 grep 但偶尔被正则卡住的在职运维和开发。
1. 写在前面:grep 为什么值得单独花一篇讲
1.1 从 RHCSE 考核要求看 grep 的定位
RHCSE 体系里的 Shell 模块,考的从来不是“背命令”,而是在真实环境里能不能快速定位问题。文本处理是运维工作中绕不过去的一环,而 grep 往往是这类操作的第一步。比如题目给你一个服务起不来,让你排查监听端口怎么没了,你的第一反应大概率是 ss -lntp,然后管道接一个 grep 8080;再比如给你一份带注释和空行的配置文件,让你找出有效配置,第一反应通常也是 grep -v '^#' 再过滤空行。这些场景,grep 虽然只是中间一环,但少了它,整条命令链就断在那里。
备考的时候不要小看这种“不够酷”的基础命令。RHCSE 机考环境里,系统不会给你任何图形界面辅助,所有的排查都靠命令行,而命令行里八成以上的文本筛选动作都由 grep 完成。可以说 grep 熟练度直接影响你在考试环境里的操作速度。很多同学不是不会 grep,而是到了压力场景下一紧张,选项就记混了,比如 -E 和 -F 用反,或者把 BRE 和 ERE 的写法搞错。这一篇就是把这些问题一次性理顺。
1.2 grep 解决的核心问题就两个字:筛选
用一句话说清楚 grep 在干什么:从一批文本里,按照某种规则把符合条件的行挑出来。你可以把文本来源想象成一个水池,grep 就是一张滤网。上游可以是文件、命令输出,甚至是另一条命令的结果。这种“小步快跑”的组合方式正是 Unix 哲学里最典型的用法——每个工具只干一件事,干好一件,然后通过管道拼起来。
grep 的英文全称是 Global search Regular expression and Print,翻译过来就是“全局搜索正则表达式并打印”。注意“正则表达式”这五个字,它和 grep 是强绑定的。很多人把 grep 当成普通的关键词搜索用,这当然没错,但如果你没把正则这一层打通,grep 的能力大概只发挥了三分之一。后面我会专门用一章讲正则,因为这是从“会用 grep”到“用熟 grep”的分水岭。
1.3 哪些人、哪些场景离不开 grep
我自己的体会是,grep 的使用场景可以粗略分成四类,基本覆盖了日常工作的所有高频需求。
第一类是日志排障。服务突然挂了,第一件事就是去翻日志,而日志文件动辄几百兆甚至几个 G,你不可能从头看到尾。这时候 grep -i error 或者 grep -iE "error|timeout" 就是最优解。第二类是进程与端口排查。比如 8080 端口被占用,sudo ss -lntp | grep 8080 一条命令就能定位到 PID 和进程名。第三类是 Shell 脚本开发。脚本里经常需要判断某个服务是否在跑、某行配置是否存在、某个进程结果是否落盘,这些都离不开 grep 的退出码或者输出结果。第四类是移动端调试。Android 的 adb shell 底下也是 Linux 环境,dumpsys 这类命令输出极其冗长,习惯性接一个 grep 过滤关键字,可读性瞬间提升一个档次。可以说,只要你跟 Linux 命令行打交道,grep 就是那件“每天都要穿的衣服”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把 grep 选项玩明白:过滤不只是精确匹配
2.1 五个最高频的基础选项
grep 的选项加起来有三四十个,但我自己天天用的其实就那几个。按照使用频率排个序,下面这个表格基本可以覆盖 90% 的日常需求。
| 选项 | 作用 | 典型示例 |
|---|---|---|
| -i | 忽略大小写 | grep -i error app.log |
| -v | 反向匹配,输出不符合条件的行 | grep -v '^#' config.conf |
| -n | 显示行号 | grep -n timeout app.log |
| -r | 递归搜索目录下的所有文件 | grep -r listen /etc/httpd/ |
| -o | 只输出匹配到的部分而不是整行 | grep -o '[0-9]\+' log.txt |
先说 -i,这个没什么好讲的,就是想搜的时候不区分大小写。但有一点要注意,grep -i error 会同时匹配到 error、Error、ERROR,输出结果可能比预期多,如果你需要精确匹配某个固定写法,就不要加 -i,或者用后面会讲的 -w 限定整词。
再说 -v,这是个看似简单但是威力极大的选项。最经典的一个组合是去掉配置文件里的注释和空行:
bash复制grep -v '^#' /etc/ssh/sshd_config | grep -v '^$'
这条命令分两步走:第一次过滤掉所有以 # 开头的行,第二次过滤掉所有空行,剩下的就是真正生效的配置项。排查问题时这个方法特别好用,能看到服务实际加载了哪些设定。注意这里用了两个管道,grep 输出给 grep,这就是“组合拳”的雏形。
-r 适合在目录里批量搜索。比如你忘了 Nginx 某个 server_name 配置在哪个文件里,直接 grep -r "server_name" /etc/nginx/conf.d/,一秒出结果。如果文件很多,建议再加上 --include='*.conf' 之类的过滤条件,避免 grep 去翻二进制和图片文件浪费时间。
-o 是提取场景的利器。默认 grep 打印整行,但有时候你只关心匹配到的部分,比如从日志里把所有的 IP 抽出来:
bash复制grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" access.log
这样得到的结果是每一行匹配到的 IP 字符串,而不是整行日志。这个选项配合 sort、uniq -c,可以直接做简单的访问统计。
2.2 上下文输出:-A、-B、-C 的妙用
排错时只看匹配行往往不够,真正需要的是“匹配行附近发生了什么”。比如程序在某个时间点报了个异常,你得看看异常发生前日志里有什么征兆,异常后又输出了什么。grep 提供了三个上下文选项:
-A n:after,输出匹配行以及它后面 n 行-B n:before,输出匹配行以及它前面 n 行-C n:context,输出匹配行以及它前后各 n 行
举一个实际例子。生产环境上应用报了一个 NullPointerException,你想看看报错前后的日志上下文,可以这样:
bash复制grep -C 10 "NullPointerException" app.log
这样一次就能看到异常附近的 20 多行日志,比自己手动翻文件高效得多。如果只想看报错之后的堆栈,那就用 -A 15。这三个选项在写排障文档的时候几乎是必用的,截图给同事看、贴到工单里都一目了然。
2.3 多模式匹配与 -e 的细节
同一个文件里想搜多个关键词,有两条路:一是用正则表达式的“或”,二是用多个 -e 参数。推荐写法是:
bash复制grep -E "error|timeout|refused" app.log
这个应该是最常见的多关键字组合。-E 表示使用扩展正则,后面我会详细讲。另一种写法是:
bash复制grep -e error -e timeout -e refused app.log
多个 -e 之间也是“或”的关系,效果和上面一样,但每个模式都是单独的参数,特别适合模式本身比较复杂或者包含特殊字符的情况。有个细节值得注意:如果模式是 - 开头,比如你想搜 -error,直接使用会被 grep 当成选项解析,这时候就必须用 -e 来保护,写成 grep -e "-error" app.log。
另外提醒一句,-v 和多个模式组合的时候是整体取反。意思是不管你写了几个模式,-v 会筛选出“所有模式都不匹配”的行,而不是只排除第一个模式。这个逻辑要记清楚,写脚本的时候才不会踩坑。
3. 正则表达式:grep 真正强大的内核
3.1 grep 名字里带 REGEX,正则不是可选项
GNU grep 的“re”指的就是正则表达式。很多人把 grep 当普通搜索用,这没问题,但如果你不会正则,遇到稍微复杂一点的筛选需求就会卡住。举个例子,你想找出日志里所有以 192.168 开头的内网 IP,如果只靠普通字符串搜索,你就得把 192.168.1.、192.168.2. 这些前缀一个个列出来,而用正则只需要这样写:
bash复制grep -E "^192\.168\." access.log
一行解决。正则的本质是“用一套元字符规则来描述一类字符串的模式”,它会把你从一堆死板的字面搜索里解放出来。学习正则不需要一次全背下来,先从最常用的几个开始,用多了自然就记住了。
3.2 BRE 与 ERE:差的就是那几根反斜杠
这是 grep 最容易让人困惑的地方。默认情况下,grep 使用基础正则表达式(BRE,Basic Regular Expression)。在 BRE 里,一些在正则里常见的特殊字符其实被“降级”成了普通字符,要想让它们发挥特殊作用,必须加反斜杠转义。而加上 -E 之后,grep 会切换到扩展正则表达式(ERE,Extended Regular Expression),这些字符就不需要转义了。
用表格看最直观:
| 正则含义 | BRE 写法(默认 grep) | ERE 写法(grep -E) |
|---|---|---|
| 或 | | |
| |
| 一个或多个 | \+ |
+ |
| 0 个或 1 个 | \? |
? |
| 分组 | \(abc\) |
(abc) |
| 区间重复 | \{3,\} |
{3,} |
这个表格值得严格记一下,因为它的坑特别隐蔽。比如你想匹配 1 个或多个数字后面跟字母 a,在 ERE 下写 [0-9]+a 就行。但如果在默认的 BRE 模式下直接写 [0-9]+a,这里的 + 会被当成普通字符处理,匹配的是“一个数字、一个加号、一个字母 a”这种字面内容,比如 1+a。结果和你想要的天差地别。
所以我个人的习惯是:只要涉及正则元字符比较多的场景,一律直接用 grep -E,尽量减少在两种模式之间来回切换的认知负担。RHCSE 备考也一样,你不需要把 BRE 和 ERE 的每个细节都背下来,但必须能在看到一条命令时判断出来它用的是哪种模式,否则排查问题会走很多弯路。
3.3 不想用正则怎么办:-F 是硬核救星
有时候你搜的内容里恰好含有点、星号、中括号这些正则元字符,比如你要在配置说明里搜 server.name = * 这个字符串。如果直接把它当成模式传给 grep,* 在正则里是有特殊含义的,结果完全不是你想要的。这时候有两个办法:一是把每个特殊字符都用反斜杠转义,太麻烦;二是直接告诉 grep “我这不是正则,就是一段普通字符串,你别给我解释”,这就是 -F 选项。
bash复制grep -F "server.name = *" config_notes.txt
-F 等价于 fgrep,它会把所有模式当作固定字符串来逐字匹配。很多人搜“grep 不要正则匹配”,要的就是这个功能。判断什么时候该用 -F 有个简单标准:如果搜索内容里出现了 .、*、[、]、^、$ 这些字符,而且你确实是想按字面意思匹配,直接上 -F,别犹豫。这个习惯能帮你省掉大量转义引号的时间。
4. Shell 脚本里 grep 的联动姿势
4.1 条件判断:grep -q 的退出码用法
grep 的退出码有三个常用值:0 表示找到了匹配,1 表示没找到,2 表示命令本身出错(比如文件不存在)。这个特性让 grep 可以直接用做 Shell 脚本里的条件判断。很多人写脚本检查服务是否在运行,会用 ps aux 输出后再接一堆命令,其实用 grep 的退出码最干净:
bash复制if grep -q "ready" /tmp/status.txt; then
echo "服务状态正常"
else
echo "服务尚未就绪"
fi
这里的关键是 -q,它让 grep 进入静默模式,不向终端输出任何匹配内容,只用退出码告诉你结果。这样做有两个好处:一是脚本运行时不产生多余输出,日志干净;二是避免把不相关的文本打到屏幕上干扰判断。需要说明的是,这种用法适合“判断某个文本是否存在”的场景,如果文件不存在,-q 模式不会输出错误提示,退出码是 2,脚本里如果没处理这种状态,可能和你预想的不一样。稳妥的做法是前面先判断文件是否存在,或者用 2>/dev/null 吞掉错误。
4.2 数据提取与 for 循环的组合
Shell 脚本里,grep 的输出经常作为 for 循环的输入源。最典型的场景是提取日志里的 IP 列表,然后逐个处理:
bash复制for ip in $(grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" access.log | sort -u); do
echo "处理IP: $ip"
done
这条命令先通过 grep -oE 从日志里抽出所有 IP 字符串,sort -u 去掉重复项,然后把每个 IP 作为循环变量处理。写这种脚本的时候有个容易踩的坑:如果匹配结果里包含空格,for 循环的默认分隔符就是空格,可能导致一个变量被拆成多个值。比如你要提取的字段本身含空格,最好先用 tr、cut 或者临时文件把格式处理好,再做循环。
另外,grep 和 awk 经常一起出现。grep 负责“选行”,awk 负责“取列”。比如你想从日志里筛出 500 错误的请求,再提取出请求时间:
bash复制grep " 500 " access.log | awk '{print $4}'
这种组合在日志分析里太常用了,值得刻意练一练。两者的分工很清晰:grep 先缩小范围,awk 再做结构化处理,比单纯用其中任何一个都高效。
4.3 实时日志过滤与端口排查实例
grep 一个很经典的场景是配合 tail -f 做实时日志监控。线上服务出问题的时候,你盯着日志文件不停刷新,其实可以用一条命令就完成任务:
bash复制tail -f /var/log/nginx/error.log | grep -i timeout
这条命令会持续输出 error 日志里包含“timeout”关键字的行。tail -f 的作用是不断读取文件新增的内容,管道把每一行都交给 grep 过滤,于是屏幕上只留下你关心的内容。这个用法在压测、联调、线上排障时非常实用,比肉眼刷日志强太多了。
端口排查也是一个高频场景。8080 端口被占用,用 ss 配合 grep 一查便知:
bash复制sudo ss -lntp | grep 8080
这行命令里,ss -lntp 分别代表:列出监听状态的 TCP 端口(-l 监听、-n 数字显示不反解域名、-t 只看 TCP、-p 显示进程信息),然后管道给 grep 过滤出 8080 相关的行。输出里会直接显示 PID 和进程名,比如“users:(("java",pid=12345,fd=13))”这样。类似的组合还有 ps aux | grep nginx、netstat -an | grep ESTABLISHED 等,思路完全一样。
5. 我踩过的坑:常见问题与排查思路
5.1 ps aux | grep 时 PID 为什么会一直变
这个问题在搜索热词里出现过,也是很多新手第一次用 ps aux | grep 时的疑惑。你执行:
bash复制ps aux | grep myscript.sh
输出里出现了两行结果,其中一行是脚本本身,另一行是一个 PID 每次都在变的“grep myscript.sh”进程。原因很简单:管道里的 grep 命令本身也是一个进程,它的命令行里包含了“myscript.sh”这个字符串,所以也被 ps aux 列了出来。每次执行都会新开一个 grep 进程,所以 PID 当然一直在变。
有三种解决办法。第一种是在管道后面再接一个 grep -v grep,把自带进程排除掉:
bash复制ps aux | grep myscript.sh | grep -v grep
第二种是使用“[m]yscript.sh”这种把首字母放进字符类的技巧:
bash复制ps aux | grep "[m]yscript.sh"
grep 自身进程的命令行是 grep [m]yscript.sh,它不等于正则要匹配的 myscript.sh,所以不会把自己算进去。第三种最推荐,直接用 pgrep:
bash复制pgrep -f myscript.sh
pgrep 本来就是干这个的,不需要再过滤,如果在脚本里做判断,pgrep -f myscript.sh > /dev/null 甚至可以直接配合退出码使用。
5.2 grep 匹配不到内容的排查顺序
很多同学遇到“明明文件里就有这个字符串,但 grep 就是搜不到”的情况,第一反应是怀疑 grep 有问题。按照我自己的经验,按下面这个顺序排查基本都能找到原因。
先确认文件路径和权限。用 ls -l 看一眼文件是否存在、是否有读权限,权限不够时 grep 会提示 Permission denied 或者 No such file。然后确认大小写。如果搜索的关键字是 Error,文件里写的是 ERROR,直接搜肯定搜不到,加上 -i 再看一眼。
接着检查正则元字符。这是最常见的情况:搜索内容里有点号或者星号,被正则解释掉了。比如你想搜 192.168.1.1,如果写成 grep 192.168.1.1 file,这里的点号其实代表“任意字符”,虽然大概率还是能匹配到,但碰巧有人写了 192x168x1x1 也会被匹配进来。如果你要精确搜 IP,记得对点号转义 192\.168\.1\.1,或者直接 grep -F "192.168.1.1"。
再确认是否需要递归搜索。如果你给的是一个目录而不是文件,grep 默认会报错,需要加 -r 或者指定具体文件。最后检查编码。如果文件是 GBK 编码或者包含很多二进制内容,grep 处理起来可能表现异常。用 file 命令看一眼文件类型,如果是二进制,可以加 -a 强制按文本处理,或者先 iconv 转码再搜索。
5.3 其他容易忽略的细节
第一个细节是引号问题。Shell 里双引号和单引号的区别在 grep 场景里非常关键。双引号内的 $、反引号、反斜杠会被 Shell 解释,单引号内的内容原样传给 grep。如果你要搜的字符串里有 $ 符号,最好用单引号,否则 Shell 会把变量展开成一个空值,grep 搜了半天搜不到。
第二个细节是 grep -c 统计的是“匹配的行数”,不是“匹配的次数”。如果一行里出现了三次 error,grep -c error file 返回的是 1 而不是 3。要统计出现次数,需要用 grep -o error file | wc -l。这个区别在写统计脚本的时候经常出问题。
第三个细节是 grep 在管道里的退出码。管道 cmd1 | grep xxx | cmd3 的整体退出码默认是最后一个命令的退出码。如果你在脚本里用了 set -e,某一步 grep 没匹配到任何内容时,它的退出码是 1,可能导致脚本提前退出。要防止这种情况,可以给 grep 加上 || true,或者临时关闭 set -e。不过在比较新的 Shell 里,可以用 set -o pipefail 来保留管道中第一个失败命令的退出码,但这个行为并不直观,建议在实际场景里先做小实验再决定怎么用。
5.4 常见问题速查表
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| ps aux 里 grep 自己每次 PID 都在变 | grep 进程被自己匹配到了 | grep -v grep、[首字母] 技巧或 pgrep |
| 搜索内容明明存在却搜不到 | 大小写、正则元字符被解释、编码问题 | 加 -i;用 -F 固定字符串;file 查看编码 |
| grep 目录时报错 | 没有加递归选项 | 加 -r 或指定具体文件 |
想搜 a.b 结果搜出来一堆乱匹配 |
点号被正则解释为任意字符 | grep -F "a.b" 或转义为 a\.b |
| 统计结果和预期不符 | -c 统计的是行数不是次数 |
grep -o xxx | wc -l |
| 脚本因 grep 没匹配到内容提前退出 | grep 退出码为 1,set -e 生效 |
加 ` |
模式以 - 开头被当成选项 |
grep 解析了选项前缀 | 使用 grep -e "-xxx" |
最后再分享一个我自己的习惯。凡是涉及“筛选”的场景,我会先想清楚:我到底要过滤“符合条件”的行,还是“不符合条件”的行?这两个角度一旦搞反,用 -v 就是一条命令的事,搞反了会在错误思路上绕很久。学 grep 不要只停留在背选项,多拿真实日志练手,把管道组合变成肌肉记忆,比什么教程都管用。
