这个命令是干啥的
grep 的全称是 Global Regular Expression Print,全局正则表达式打印。它的核心能力是在文本中搜索匹配特定模式的行并输出。
很多人对 grep 的认知就是 grep ERROR app.log,但说实话,这只发挥了 grep 10% 的能力。真正的 grep 高手懂得看上下文、递归搜索、匹配计数、排除无关目录,在几 G 的日志里几秒找到问题。
我刚开始做运维那会儿,每次线上出问题就在那 cat app.log | grep ERROR,然后被海量的 ERROR 刷屏,根本找不到真正的根因。后来学会加 -B -A 看上下文,才知道出问题之前发生了什么。
基本用法(3分钟上手)
# 基本搜索
grep "error" file.txt
# 忽略大小写
grep -i "error" file.txt
# 显示行号
grep -n "error" file.txt
# 统计匹配行数
grep -c "error" file.txt
# 反向匹配(不含 error 的行)
grep -v "error" file.txt
# 递归搜索目录
grep -r "error" /var/log/
进阶骚操作
1. 看上下文:-A -B -C
这是 grep 最重要的参数组合。查错误日志只看 ERROR 这一行根本不够,得知道它前后的日志。
# 显示匹配行和后2行
grep -A 2 "java.lang.NullPointerException" app.log
# 显示匹配行和前3行
grep -B 3 "FATAL" app.log
# 显示匹配行前后各3行
grep -C 3 "OutOfMemoryError" app.log
# 显示前后各5行,同时显示行号
grep -n -C 5 "CRITICAL" system.log
每次我看到有人只用 grep ERROR app.log 查日志,我都会让他试试 grep -C 5 ERROR app.log。加了上下文之后,很多问题一看就明白了。比如一个数据库连接超时,上面几行可能是一个慢查询,下面几行是连接池状态,结合起来才能定位问题。
2. 只列文件名:-l 和 -L
当你想知道哪个文件包含某个关键词时:
# 列出包含 error 的文件名(不显示内容)
grep -l "error" /var/log/*.log
# 列出不包含 error 的文件名
grep -L "error" /var/log/*.log
# 递归搜索目录,只列文件名
grep -rl "192.168.1.100" /etc/
我排查配置问题的时候常用 grep -rl。比如在 /etc/nginx/ 下搜索某个域名配置在哪个文件里,-rl 效率极高。
3. 扩展正则:-E
# 匹配多个模式
grep -E "ERROR|FATAL|CRITICAL" app.log
# 匹配行首
grep -E "^ERROR" app.log
# 匹配数字
grep -E "[0-9]{4}-[0-9]{2}-[0-9]{2}" app.log
# 匹配 IP 地址
grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" access.log
-E 支持完整的扩展正则表达式,基本能解决 90% 的文本匹配需求。不用 -E 的话,很多特殊字符得加反斜杠转义,很麻烦。
4. 只用匹配的部分:-o
# 只显示匹配到的 IP 地址本身,而不是整行
grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" access.log
# 统计 IP 出现次数
grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" access.log | sort | uniq -c | sort -rn | head -10
-o 配合数据分析场景特别好用。比如从一堆混杂的日志里提取出所有邮箱、IP、URL,然后做统计。
5. 排除目录:--exclude-dir
递归搜索时排除不必要的目录:
# 排除 node_modules 和 .git
grep -r "console.log" --exclude-dir={node_modules,.git} .
# 排除多个目录
grep -r "TODO" --exclude-dir={node_modules,.git,vendor,build} .
# 排除特定文件
grep -r "API_KEY" --exclude="*.min.js" --exclude="*.map" .
我第一次在项目里 grep -r "password" .,结果 node_modules 里的几十万个文件全搜了一遍,等了快一分钟才出结果。加 --exclude-dir=node_modules 后,两三秒就完了。
6. 几种常用的 grep 模式
# 匹配完整单词
grep -w "error" file.txt # 不会匹配到 "errors" "error-handler"
grep -w "404" access.log # 不会匹配到 "1404" "4041"
# 查询二进制文件中的字符串(比如查可执行文件里有没有某个配置)
grep -a "password" binary_file
-w 匹配完整单词,查代码和日志时特别有用。比如你搜 error,如果不加 -w,errorHandling、errorCode、nonerror 都会匹配到。
避坑指南
坑1:grep 大小写敏感
# 搜不到 "ERROR" 因为文件里是 "Error"
grep "ERROR" app.log
# 正确做法
grep -i "error" app.log
大多数日志是大小写敏感的,但 grep 默认也是大小写敏感。忘加 -i 就漏掉了。我一般习惯在日志搜索时默认加 -i。
坑2:grep 匹配结果被截断
# grep 匹配到的行如果太长,会输出完整一行
# 但终端宽度有限,一行太长会换行显示
grep "very long error message" app.log
长行在终端显示效果很差。解决方案:
# 用 -o 只显示匹配部分
grep -o "very long error message.*" app.log
# 或者用 less 分页
grep "ERROR" app.log | less -S # -S 不换行,左右滚动
坑3:grep -r 遇到符号链接
# -r 默认不跟踪符号链接,如果 /var/log 是软链接可能搜不到
grep -r "ERROR" /var/log/
# 用 -R 跟踪符号链接
grep -R "ERROR" /var/log/
-r 和 -R 的区别:-r 不追踪符号链接目录,-R 会追踪。如果日志目录通过软链接指向另一个位置,用 -R。
坑4:特殊字符的转义
# 搜索包含 ". " 的行会失败,因为 . 是正则通配符
grep "192.168.1.1" access.log
# 这个会匹配 "192x168y1z1" 之类的内容
# 正确做法:转义 . 或者用 -F 固定字符串
grep -F "192.168.1.1" access.log
grep "192\.168\.1\.1" access.log
-F 参数告诉 grep 把搜索模式当成固定字符串,不解释正则。如果搜索的是纯文本(不是正则模式),加 -F 不仅准确而且速度更快。
坑5:大文件不够快
几 G 的日志文件,用 grep 匹配确实慢:
# 这个在 5G 文件上可能要几分钟
grep "ERROR" 5gig_log.log
# 优化方式
# 1. 如果只要少量结果,找到就停
grep -m 10 "ERROR" 5gig_log.log
# 2. 用 LC_ALL=C 加速(不处理 Unicode)
LC_ALL=C grep "ERROR" 5gig_log.log
# 3. 用 ripgrep 替代(如果有的话)
rg "ERROR" 5gig_log.log
LC_ALL=C 告诉 grep 不需要处理多字节字符,对小文件没用,但大文件能快几倍。-m 限制匹配数量,找到 N 个就停,不需要扫完整个文件。
关于 ripgrep (rg)
如果你经常在大项目里搜索文本,强烈推荐 ripgrep:
# 安装
apt install ripgrep # Ubuntu/Debian
brew install ripgrep # macOS
# 用法(和 grep 类似但快很多)
rg "TODO" --type-add 'web:*.{html,css,js}' -t web
rg "function" --no-ignore # 不忽略 .gitignore 里的文件
rg "import" --stats # 显示搜索统计信息
ripgrep 默认就排除 .gitignore 里的目录,递归搜索效率极高。拿 100 万行代码的项目来说,grep 可能要 10 秒,rg 不到 1 秒。
实战场景(重点!结合真实运维场景)
场景1:快速定位 Java OOM 问题
线上 Java 服务突然报了 OOM,快速排查:
#!/bin/bash
# Java 内存问题排查脚本
LOG_FILE="/var/log/myapp/app.log"
echo "=== 查找 OOM 相关日志 ==="
# -A 2: 显示 OutOfMemoryError 后2行(看堆栈头部)
# -B 5: 显示前5行(看上下文)
grep -A 2 -B 5 "OutOfMemoryError" "$LOG_FILE"
echo ""
echo "=== 查找最近 GC 相关日志 ==="
# GC 日志通常在独立文件或包含 GC 标记
grep -i "gc\|garbage collection" "$LOG_FILE" | tail -10
echo ""
echo "=== 当时的内存使用情况 ==="
# 搜索 OOM 发生前后的内存使用记录
grep -B 20 "OutOfMemoryError" "$LOG_FILE" | grep -i "memory\|heap\|usage"
场景2:分析 Nginx 访问日志的异常请求
#!/bin/bash
# Nginx 日志分析
ACCESS_LOG="/var/log/nginx/access.log"
echo "=== 4xx 请求统计 ==="
grep -E 'HTTP/1\.[01]" [45][0-9]{2}' "$ACCESS_LOG" | \
grep -oE '" [0-9]{3} ' | sort | uniq -c | sort -rn
echo ""
echo "=== 5xx 错误最多的 URL ==="
grep -E '" 5[0-9]{2} ' "$ACCESS_LOG" | \
grep -oE '(GET|POST|PUT|DELETE) [^ ]+' | \
sort | uniq -c | sort -rn | head -10
echo ""
echo "=== 被 403 禁止的 IP ==="
grep -E '" 403 ' "$ACCESS_LOG" | \
grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | \
sort | uniq -c | sort -rn | head -10
这套分析脚本帮我发现过多次恶意扫描。某个 IP 访问了大量不存在的 URL 导致 404,或者某个路径出现大量 500 错误说明代码有问题。
场景3:代码审查:找遗留的调试代码
#!/bin/bash
# 发布前检查项目代码中是否有遗留的调试代码
echo "=== 检查遗留的 console.log ==="
grep -rn "console\.log\|console\.debug" src/ --exclude-dir=node_modules --exclude="*.test.js"
echo ""
echo "=== 检查遗留的 TODO ==="
grep -rn "TODO\|FIXME\|HACK\|XXX" src/ --exclude-dir=node_modules
echo ""
echo "=== 检查硬编码的密码或密钥 ==="
grep -rn -E "(password|secret|api_key)\s*[:=]\s*['\"][^'\"]+['\"]" \
config/ --exclude-dir=node_modules
场景4:统计日志中各类错误的分布
#!/bin/bash
# 按错误类型统计
LOG_FILE="/var/log/myapp/app.log"
echo "=== 各类错误分布 ==="
# 假设日志格式:2024-06-22 10:00:00 [ERROR] [模块名] 错误描述
grep "\[ERROR\]" "$LOG_FILE" | \
# 提取模块名
grep -oP '\[\w+\] [\w_]+:' | \
sort | uniq -c | sort -rn | head -20
echo ""
echo "=== 过去一小时内新增的错误 ==="
# 获取当前时间戳,匹配过去一小时
grep "$(date -d '1 hour ago' '+%H:%M')" "$LOG_FILE" | grep -c "ERROR"
今日作业
你的 Java 应用日志文件 /var/log/tomcat/catalina.out 有 2G 大小,里面包含了大量的堆栈信息。
需要写一条命令:
1. 搜索所有包含 Exception 的行(忽略大小写)
2. 显示匹配行的前 5 行和后 3 行上下文
3. 忽略大小写
4. 限制最多输出 20 个匹配结果
5. 显示匹配结果的行号
(提示:组合 -i -C -m -n 参数。注意 -C 和 -m 同时用的效果。)