grep 搜索实战
# 第 4 章 grep 搜索实战
# 目录介绍
# 4.1 grep 基础搜索
# 4.1.1 基本用法——三种调式
grep = Global Regular Expression Print——从文本中搜索匹配正则表达式的行并打印。它是 Shell 文本处理中最常用的命令:
# ===== 调式 1:最基本的搜索 =====
grep "ERROR" app.log # 搜索含有 ERROR 的行
# ===== 调式 2:从管道接收输入 =====
cat app.log | grep "ERROR" # 效果同上(但多了无意义的 cat)
ps aux | grep nginx # 查找 nginx 进程
dmesg | grep "usb" # 从内核日志中搜索 USB
# ===== 调式 3:从多个文件搜索 =====
grep "ERROR" app.log app.log.1 app.log.2 # 多个文件——会显示文件名
grep "ERROR" /var/log/*.log # 通配符——搜索所有 .log 文件
# ===== 实战:三种使用方式最佳实践 =====
# ✅ 场景 1:搜索单个文件
grep -n "timeout" /etc/nginx/nginx.conf
# ✅ 场景 2:从管道接收——grep 作为过滤器
tail -f /var/log/app.log | grep "ERROR" # 实时监控并过滤错误
# ✅ 场景 3:搜索整个目录
grep -rn "TODO" /home/user/project/ --include="*.py" # 在 Python 源码中搜索 TODO
🔑 grep 的三种变体——本质是同一个命令的快捷方式:
| 命令 | 等价于 | 正则类型 | 记忆 |
|---|---|---|---|
grep | grep -G | 基础正则(BRE) | g/re/p 的缩写 |
egrep | grep -E | 扩展正则(ERE) | Extended |
fgrep | grep -F | 固定字符串(无正则) | Fixed string |
# egrep = grep -E(扩展正则——最常用)
grep -E "[0-9]{3}-[0-9]{4}" file.txt # 匹配电话号码
# fgrep = grep -F(固定字符串——最快)
grep -F "a.txt" file.txt # 把 .* 等当作文本字面量搜索
# 当你要搜索的字符串包含大量正则元字符时——用 -F 避免转义地狱
# 4.1.2 正则基础 . ^ $ * + ?
正则表达式是 grep 的灵魂——掌握元字符就能精准定位任何文本:
#!/bin/bash
# ===== . —— 匹配任意单个字符(除换行符)=====
echo -e "cat\ncut\ndog" | grep "c.t" # 匹配:cat cut(不匹配 dog)
# . 匹配 t、u——所以 c.t 匹配 "cat" 和 "cut"
# ===== ^ —— 行首锚定 =====
grep "^ERROR" app.log # 匹配以 ERROR 开头的行
grep "^[[:space:]]*#" config.conf # 匹配被空格缩进的注释行
# ===== $ —— 行尾锚定 =====
grep "error$" app.log # 匹配以 error 结尾的行
grep "^$" app.log # 匹配空行(行首后紧跟行尾)
# ===== * —— 前一个字符重复 0 次或多次 =====
grep "ab*c" file.txt # 匹配:ac, abc, abbc, abbbc...
# 实际含义:a + b(0次或多次) + c
# ===== + —— 前一个字符重复 1 次或多次(需要 -E)=====
# grep 基础模式不支持 +——必须加 -E
grep -E "ab+c" file.txt # 匹配:abc, abbc, abbbc...(最少 1 个 b)
# ===== ? —— 前一个字符重复 0 次或 1 次(需要 -E)=====
grep -E "colou?r" file.txt # 匹配:color, colour
# ===== [ ] —— 字符集 =====
grep "gr[ae]y" file.txt # 匹配:gray, grey
grep "[0-9]" file.txt # 包含数字的行
grep "[a-zA-Z]" file.txt # 包含英文字母的行
grep "[^0-9]" file.txt # 包含非数字字符的行(^ 在 [] 开头表示取反)
# ===== 预定义字符类 =====
grep "[[:space:]]" file.txt # 空白字符(空格/tab等)
grep "[[:digit:]]" file.txt # 数字 [0-9]
grep "[[:alpha:]]" file.txt # 字母 [a-zA-Z]
grep "[[:alnum:]]" file.txt # 字母+数字
grep "[[:punct:]]" file.txt # 标点符号
🔑 正则元字符速查表:
| 元字符 | 含义 | 示例 |
|---|---|---|
. | 任意单个字符 | h.t → hat, hit, hot |
^ | 行首 | ^# → 注释行 |
$ | 行尾 | $ → 行尾锚定 |
* | 0 次或多次 | ab*c → ac, abc, abbc |
+ | 1 次或多次(-E) | ab+c → abc, abbc |
? | 0 次或 1 次(-E) | colou?r → color, colour |
{n,m} | n 到 m 次(-E) | [0-9]{3,5} → 3-5 位数字 |
[abc] | 字符集 | gr[ae]y → gray, grey |
[^abc] | 字符集取反 | [^0-9] → 非数字 |
( ) | 分组(-E) | (ab)+ → ab, abab |
\| | 或(-E) | cat\|dog → cat 或 dog |
\b | 单词边界 | \bword\b → 单词 word |
# ===== 字符集 {n,m} 量词(需要 -E)=====
grep -E "[0-9]{3}" file.txt # 连续 3 位数字
grep -E "[0-9]{4,}" file.txt # 至少 4 位数字
grep -E "[a-z]{3,5}" file.txt # 3-5 位小写字母
# ===== | 或(需要 -E)=====
grep -E "ERROR|FATAL|CRITICAL" app.log # 搜索三种严重级别
# ===== 分组 ( )(需要 -E)=====
grep -E "(http|https)://" url.txt # http:// 或 https://
grep -E "(ab){2,}" file.txt # ab 重复至少 2 次:abab, ababab...
# ===== 转义元字符——用 \ 取消特殊含义 =====
grep "1\.2\.3\.4" ip.txt # 搜索精确字符串 "1.2.3.4"(. 被转义为字面量)
# 4.1.3 常用选项速查
#!/bin/bash
# ===== -i —— 忽略大小写 =====
grep -i "error" app.log # 匹配:error, Error, ERROR, eRrOr
# ===== -v —— 反向匹配(显示不匹配的行)=====
grep -v "^#" /etc/nginx/nginx.conf # 显示非注释行
ps aux | grep -v "grep" # 去掉 grep 命令自身进程
# ===== -n —— 显示行号 =====
grep -n "listen" /etc/nginx/nginx.conf # 输出:12:listen 80;
# ===== -r / -R —— 递归搜索 =====
grep -rn "TODO" /home/user/project/ # 递归搜索整个目录——显示文件名+行号+内容
# ===== -l / -L —— 只显示文件名 =====
grep -rl "class User" /home/user/project/ # 只列出包含匹配的文件名(不显示内容)
grep -rL "class User" /home/user/project/ # 只列出不包含匹配的文件名
# ===== -c —— 统计匹配行数 =====
grep -c "ERROR" app.log # 输出:42(匹配的行数,不是次数)
# ===== -o —— 仅输出匹配部分 =====
echo "error at 2025-06-07 14:30:00" | grep -o "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}"
# 输出:2025-06-07
# ===== -s —— 静默模式(不显示文件不存在的错误)=====
grep -s "ERROR" /nonexistent/file.log # 不输出错误——适合在脚本中使用
🔑 常用选项速查表:
| 选项 | 含义 | 场景 |
|---|---|---|
-i | 忽略大小写 | 搜索时不分大小写 |
-v | 反向匹配 | 过滤掉不需要的行 |
-n | 显示行号 | 快速定位到原文 |
-r | 递归搜索目录 | 在项目中全局搜索 |
-l | 只显示文件名 | 知道哪些文件涉及 |
-L | 不匹配的文件名 | 找没有某模式的文件 |
-c | 统计行数 | 快速计数 |
-o | 仅显示匹配部分 | 提取特定信息 |
-s | 不显示不存在错误 | 脚本中静默使用 |
-q | 安静模式(不输出) | 只关心退出码 |
-w | 整词匹配 | 避免部分匹配 |
-x | 整行匹配 | 搜索精确行内容 |
-q 在脚本中的用法——只关心是否匹配到:
#!/bin/bash
if grep -q "pattern" /var/log/app.log; then
echo "找到了"
fi
# 等价于(但 -q 更快——找到第一个就停止)
grep "pattern" /var/log/app.log > /dev/null 2>&1
# 4.2 grep 高级技巧
# 4.2.1 扩展正则 -E
基础正则(BRE)和扩展正则(ERE)的核心区别——哪些元字符需要转义:
# ===== BRE vs ERE 对比 =====
# BRE 中 + ? { } ( ) | 都被当作字面量——需要 \ 转义才获得特殊含义
# ERE 中这些字符默认就是特殊含义
# BRE(grep 默认)——元字符要加反斜杠
grep "\+" file.txt # 搜索 + 符号本身
grep "\?" file.txt # 搜索 ? 符号本身
# 那在 BRE 中怎么用 + 或 ? 的含义?
grep "ab\+c" file.txt # 匹配 abc, abbc...(\+ 是"1次或多次")
grep "colou\?r" file.txt # 匹配 color, colour
# ERE(grep -E)——元字符不需要反斜杠
grep -E "ab+c" file.txt # 匹配 abc, abbc...
grep -E "colou?r" file.txt # 匹配 color, colour
# ===== 综合对比 =====
模式 BRE (grep) ERE (grep -E)
数字 3-5 位 grep "[0-9]\{3,5\}" grep -E "[0-9]{3,5}"
IP 地址 grep "[0-9]\{1,3\}\.[0-9]\{1,3\}" grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}"
cat 或 dog grep "cat\|dog" grep -E "cat|dog"
📌 新写 grep 命令一律用
-E——ERE 的可读性远高于 BRE,没有理由再写反斜杠地狱。
# 4.2.2 单词边界 \b 与词锚定
\b = word boundary——匹配单词的开头或结尾,避免部分匹配:
#!/bin/bash
# ===== 没有 \b——部分匹配的问题 =====
echo -e "cat\ncatalog\ncategory\ncat\ncat" | grep "cat"
# 输出:cat, catalog, category, cat, cat(包含 cat 的行全部匹配)
# ===== 有 \b——严格的单词匹配 =====
echo -e "cat\ncatalog\ncategory\ncat" | grep "\bcat\b"
# 输出:cat, cat(只匹配完整单词 "cat")
# ===== 行首/行尾锚定 vs 单词边界 =====
grep "^cat" file.txt # 行首的 cat——只能匹配行首
grep "cat$" file.txt # 行尾的 cat——只能匹配行尾
grep "\bcat\b" file.txt # 任何位置的完整单词 cat——最灵活
# ===== 兼容写法——没有 \b 时的替代 =====
# grep 的 \< 和 \> 等价于 \b(GNU grep 支持)
grep "\<cat\>" file.txt # 等价于 grep "\bcat\b"
🔑 单词边界实用模式:
# 搜索完整单词 "log"(不会匹配到 blog, catalog, dialog)
grep -w "log" file.txt # -w = 整词匹配(等价于 \blog\b)
# 搜索以 error 开头的单词
grep -E "\berror" app.log # matches: error, errorlog(不匹配 aerror)
# 搜索以 ing 结尾的单词
grep -E "ing\b" app.log # matches: running, walking(不匹配 ingredient)
# 搜索 3 位数字单词(独立数字)
grep -E "\b[0-9]{3}\b" file.txt # 匹配 123, 456(不匹配 1234 或 0123)
# 4.2.3 后向引用与捕获组 \1 \2
捕获组 () 不仅能分组——还能"记住"匹配到的内容,在后面引用:
#!/bin/bash
# ===== 后向引用基础——查找重复单词 =====
echo "the the quick brown fox" | grep -E "(\b\w+\b) \1"
# 输出:the the(\1 引用第一个捕获组匹配到的内容)
# ===== 查找相邻重复单词(写作中的常见错误)=====
echo -e "hello world\nhello hello world\nthis this is a test" | grep -E "(\b\w+\b) \1"
# 输出:
# hello hello world
# this this is a test
# ===== 匹配成对标签 =====
echo "<h1>Title</h1><p>Text</p>" | grep -E "<([a-z]+)>.*</\1>"
# \1 确保关闭标签和打开标签同名
# 匹配 <h1>Title</h1> 和 <p>Text</p>
# ===== 匹配重复的 IP 地址段 =====
echo "192.192.168.1" | grep -E "([0-9]{1,3})\.\1" # 找到前两段相同的 IP
🔑 后向引用实际应用:
# ===== 查找 HTML 中的重复属性 =====
grep -E 'class="([^"]+)"[^>]*class="\1"' index.html # 同一个 class 出现两次
# ===== 格式化日志中的重复时间戳 =====
# 假设日志格式:2025-06-07 2025-06-07 ERROR something
grep -E "([0-9]{4}-[0-9]{2}-[0-9]{2}) \1" app.log # 两次相同日期
# ===== 捕获组 + -o 提取特定字段 =====
echo "name=张三 age=25 city=深圳" | grep -oE 'name=(\w+)' | grep -oE '\w+$'
# 输出:张三(先匹配 name=张三,再提取最后的单词)
# 4.2.4 上下文 -A / -B / -C
搜索匹配行时,通常也需要看它的前后文——grep 提供了三组选项:
#!/bin/bash
# ===== -A N —— After(匹配行后 N 行)=====
grep -A 3 "FATAL" app.log # 显示匹配行及之后 3 行
# ===== -B N —— Before(匹配行前 N 行)=====
grep -B 2 "Exception" app.log # 显示匹配行及之前 2 行
# ===== -C N —— Context(匹配行前后各 N 行)=====
grep -C 5 "CRITICAL" app.log # 显示匹配行及前后各 5 行——最常用
# ===== 分组上下文——用 --group-separator =====
grep -C 2 "ERROR" app.log --group-separator="──────────"
# 不同匹配组之间用 ────────── 分隔
🔑 实战:分析堆栈跟踪:
# Java/Python 报错时通常会有一段堆栈——-A 可以顺便捕获它
grep -A 15 "Traceback (most recent call last)" app.log
# 显示 Python 异常及其 15 行堆栈
# 显示 SQL 错误及前后上下文
grep -B 2 -A 20 "SQLSTATE" mysql.log
# 看到 SQL 语句及其错误详情
# 4.2.5 -o 仅匹配与 -c 统计——精准提取
#!/bin/bash
# ===== -o —— 仅输出匹配部分 =====
# 默认 grep 输出一整行——-o 只输出匹配到的部分
echo "error at 2025-06-07 14:30:00" | grep -oE "[0-9]{2}:[0-9]{2}:[0-9]{2}"
# 输出:14:30:00(只提取时间)
# ===== -o 配合 -E 提取 IP 地址 =====
echo "访问来自 192.168.1.1,状态 200" | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"
# 输出:192.168.1.1
# ===== -o 提取所有邮箱 =====
grep -oE "\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b" contacts.txt
# ===== -c —— 统计匹配行数 =====
grep -c "ERROR" app.log # 输出包含 ERROR 的行数
# ===== 注意:-c 是行数,不是匹配次数 =====
# 如果一行里有 3 个 ERROR——-c 只计 1
# 要统计总次数:用 -o + wc -l
grep -o "ERROR" app.log | wc -l # 统计所有 ERROR 出现的总次数
# ===== -o + 排序:提取并统计 =====
grep -oE "\b[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\b" access.log \
| sort \
| uniq -c \
| sort -rn \
| head -10
# 提取所有 IP,排序后统计每个 IP 出现的次数,取 Top 10
# 4.2.6 多模式 -e 与模式文件 -f
#!/bin/bash
# ===== -e —— 多个模式任意匹配(或关系)=====
grep -e "ERROR" -e "FATAL" -e "CRITICAL" app.log
# 搜索三个关键词中的任意一个——等价于 grep -E "ERROR|FATAL|CRITICAL"
# ===== -e 处理以 - 开头的模式 =====
grep -e "--max-age" nginx.conf # 搜索以 - 开头的字符串
# 如果不加 -e:grep "--max-age" 会被解析为选项而不是模式!
# ===== -f —— 从文件读取模式 =====
# patterns.txt 内容:
# ERROR
# FATAL
# CRITICAL
# Timeout:
grep -f patterns.txt app.log # 同时搜索文件中列出的所有模式
# ===== 模式文件的优势 =====
# ① 模式可以包含很多——不限制数量
# ② 方便管理和复用
# ③ 可以配合其他工具生成模式
# 实战:从告警配置生成模式
grep "^[^#]" alert_patterns.cfg | grep -v "^$" | grep -f - app.log
# 读取 config 中非注释非空的行为模式——-f - 从 stdin 读取模式
# 4.2.7 二进制文件与彩色输出
#!/bin/bash
# ===== 彩色输出(默认已是——否则手动加 --color)=====
grep --color=auto "ERROR" app.log # auto:管道时不输出颜色
grep --color=always "ERROR" app.log # always:即使管道也输出颜色
grep --color=never "ERROR" app.log # never:禁止颜色
# 在脚本中推荐 --color=never——颜色代码会污染管道输出
# ===== 处理二进制文件 =====
grep -a "text" binary_file # -a:把二进制当文本处理
grep -I "pattern" *.log # -I:忽略二进制文件
# ===== 处理大文件——性能优化 =====
# --line-buffered:逐行输出(适合 tail -f 实时监控)
tail -f app.log | grep --line-buffered "ERROR"
# --max-count:找到 N 个匹配后停止
grep --max-count=5 "ERROR" app.log # 只找前 5 个
# 4.3 grep 实战案例
# 4.3.1 日志分析——从百万行中找到问题
#!/bin/bash
# ===== 场景 1:统计不同严重级别的错误数量 =====
echo "=== 错误统计 ==="
for level in INFO WARN ERROR FATAL; do
count=$(grep -c "$level" /var/log/app.log)
printf " %-10s %s\n" "$level" "$count"
done
# ===== 场景 2:查找时间范围内的日志 =====
# 假设日志格式:[2025-06-07 14:30:00] [ERROR] ...
grep "2025-06-07 1[4-5]" /var/log/app.log # 14:xx 到 15:xx 的日志
# ===== 场景 3:分析 5xx 错误 =====
grep -E '" [5][0-9][0-9] ' access.log # HTTP 5xx 状态码
grep -c -E '" 5[0-9][0-9] ' access.log # 5xx 总数
grep -E '" 5[0-9][0-9] ' access.log | wc -l # 同上(-c 更快)
# ===== 场景 4:慢查询分析(超过 1 秒的请求)=====
# 假设格式:/api/users 耗时 2345ms
grep -E "耗时 [1-9][0-9]{3,}ms" app.log # 1000ms+ 的慢请求
# ===== 场景 5:查看某个用户的操作记录 =====
grep "user_id=9527" /var/log/app.log | tail -50
# ===== 场景 6:排除已知的无害错误 =====
grep -E "ERROR|FATAL" app.log | grep -v "Connection reset by peer"
# 只显示真实的错误,排除已知的安全警告
# 4.3.2 代码搜索——在项目中快速定位
#!/bin/bash
# ===== 基础代码搜索 =====
grep -rn "class User" src/ # 定位 User 类定义
grep -rn "def get_user" src/ # 定位 Python 函数定义
grep -rn "interface.*Service" src/ # 搜索包含 interface 和 Service 的行
# ===== 指定文件类型搜索 =====
grep -rn "TODO\|FIXME\|HACK" src/ --include="*.py" # Python 文件的待办
grep -rn "console.log" src/ --include="*.js" # JS 遗留调试
grep -rn "\.only\(" tests/ --include="*.test.js" # 定位跳过的测试
# ===== 排除特定目录 =====
grep -rn "api_key" . --exclude-dir=node_modules --exclude-dir=.git
grep -rn "password" . --exclude="*.min.js" --exclude="vendor/*"
# ===== 搜索 import 依赖 =====
grep -rn "^import " src/ | sort | uniq # 汇总所有 import 语句
# ===== 搜索函数调用关系 =====
grep -rn "send_email(" src/ # 找到所有调用 send_email 的地方
# 4.3.3 数据提取——从文本中提取结构化信息
#!/bin/bash
# ===== 提取 IP 地址 =====
grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" /var/log/secure | sort -u
# 从登录日志中提取所有唯一 IP
# ===== 提取邮箱 =====
grep -oE "\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b" contacts.txt
# ===== 提取 URL =====
grep -oE "https?://[a-zA-Z0-9./?=_-]+" markdown.md | sort -u
# ===== 提取数字 =====
grep -oE "[0-9]+" data.txt | paste -sd+ | bc # 求和所有数字
# ===== 提取 JSON 字段 =====
echo '{"name":"张三","age":25,"city":"深圳"}' | grep -oE '"name":"[^"]*"' | cut -d'"' -f4
# 输出:张三
# ===== 提取带前后缀的上下文 =====
# 从 HTML 中提取标题
grep -oP '<title>\K[^<]+(?=</title>)' index.html
# \K 丢弃之前匹配的内容,(?=...) 向前查找——只提取标题文本
# 4.3.4 多文件批量处理
#!/bin/bash
# ===== 批量替换前的搜索确认 =====
# 在修改前确认哪些文件会受影响
grep -rl "old_hostname" /etc/ --include="*.conf" 2>/dev/null
# 输出匹配文件列表——安全确认后再执行 sed 替换
# ===== 跨文件统计 =====
grep -rh "ERROR" /var/log/ --include="*.log" | sort | uniq -c | sort -rn | head -20
# -h:不显示文件名(合并所有匹配行)
# ===== 在多个日志文件中找最新的错误 =====
# 按修改时间排序日志文件,在每个文件里搜索
ls -t /var/log/app.log* | while read -r f; do
count=$(grep -c "ERROR" "$f" 2>/dev/null)
[[ "$count" -gt 0 ]] && echo "$f: $count 个错误"
done
# ===== 统计不同文件的错误数 =====
for f in /var/log/*.log; do
errors=$(grep -c "ERROR" "$f" 2>/dev/null || echo 0)
printf "%-40s %d\n" "$f" "$errors"
done | sort -t' ' -k2 -rn | head -10
# ===== 搜索并生成 HTML 报告 =====
cat <<'HTML' > search_report.html
<html><body><h1>搜索结果</h1><pre>
HTML
grep -rn "TODO" src/ --include="*.py" >> search_report.html
echo "</pre></body></html>" >> search_report.html
# 4.4 综合案例:错误日志监控告警脚本
把本章全部知识——基础搜索 + 扩展正则 + 上下文 + 计数 + 多文件 + 管道组合——串联成一个生产级日志监控脚本:
#!/bin/bash
# log_monitor.sh —— 错误日志监控告警脚本
# 用法:./log_monitor.sh <日志目录> [选项]
# 示例:./log_monitor.sh /var/log/myapp --severity ERROR --window 60 --alert 10
set -euo pipefail
# ===== 1. 配置 =====
declare -A CONFIG
CONFIG[log_dir]="${1:-/var/log/myapp}"
CONFIG[severity]="${2:-ERROR}"
CONFIG[time_window]="${3:-300}" # 统计时间窗口(秒),默认 5 分钟
CONFIG[alert_threshold]="${4:-10}" # 超过多少条触发告警
CONFIG[pattern_file]="" # 自定义模式文件
# 预定义严重级别模式
declare -A LEVEL_PATTERNS
LEVEL_PATTERNS[INFO]="^[0-9]{4}-[0-9]{2}-[0-9]{2}.*\[INFO\]"
LEVEL_PATTERNS[WARN]="^[0-9]{4}-[0-9]{2}-[0-9]{2}.*\[WARN\]"
LEVEL_PATTERNS[ERROR]="^[0-9]{4}-[0-9]{2}-[0-9]{2}.*\[ERROR\]"
LEVEL_PATTERNS[FATAL]="^[0-9]{4}-[0-9]{2}-[0-9]{2}.*\[FATAL\]"
# ===== 2. 函数定义 =====
# 彩色输出
RED='\033[0;31m'; GREEN='\033[0;32m'
YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
usage() {
cat <<EOF
用法:$0 <日志目录> [选项]
选项:
--severity LEVEL 搜索级别(INFO/WARN/ERROR/FATAL,默认 ERROR)
--window SEC 时间窗口(秒,默认 300)
--alert N N 条匹配时触发告警(默认 10)
--pattern "REGEX" 自定义正则模式
--show 显示详细错误行(默认只显示统计)
-h, --help 显示帮助
EOF
exit 0
}
# 时间窗口内的日志检查
check_logs_in_window() {
local log_file="$1"
local pattern="$2"
local window="$3"
# 获取最近 window 秒内的日志条数
# 假设日志时间戳格式:[2025-06-07 14:30:00]
if [[ -f "$log_file" ]]; then
local now timestamp cut_time count
now=$(date '+%s')
timestamp=$(date -d "@$((now - window))" '+%Y-%m-%d %H:%M:%S')
# 跳过时间戳比较——用 tail 代替(更简单)
# 用 wc -l 估算日志量,然后用 tail 查看末尾部分
local total_lines
total_lines=$(wc -l < "$log_file" 2>/dev/null || echo 0)
local tail_lines=500
if (( total_lines > tail_lines )); then
count=$(tail -n "$tail_lines" "$log_file" | grep -cE "$pattern" 2>/dev/null || echo 0)
else
count=$(grep -cE "$pattern" "$log_file" 2>/dev/null || echo 0)
fi
echo "$count"
return 0
fi
echo 0
}
# 显示详细的错误行(带上下文)
show_error_details() {
local log_file="$1"
local pattern="$2"
local context_lines="${3:-2}"
if [[ -f "$log_file" ]]; then
echo ""
echo "━━━ 详细错误($log_file)━━━━━━━━━━━━━"
grep -n -C "$context_lines" -E "$pattern" "$log_file" \
--color=never | head -100
echo ""
fi
}
# 发送告警(这里用 echo 模拟——实际可以替换为邮件/钉钉/飞书)
send_alert() {
local subject="$1"
local body="$2"
echo "[ALERT] $subject"
echo "$body"
# 实际使用时可替换为:
# echo "$body" | mail -s "$subject" admin@company.com
# curl -X POST -H "Content-Type: application/json" \
# -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"$subject\n$body\"}}" \
# https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY
}
# ===== 3. 主逻辑 =====
main() {
local log_dir="${CONFIG[log_dir]}"
local severity="${CONFIG[severity]}"
local window="${CONFIG[time_window]}"
local threshold="${CONFIG[alert_threshold]}"
# 检查目录
if [[ ! -d "$log_dir" ]]; then
log_error "日志目录不存在:$log_dir"
exit 1
fi
log_info "开始监控:$log_dir"
log_info "级别:$severity | 窗口:${window}s | 阈值:$threshold"
# 获取搜索正则模式
local pattern
if [[ -n "${CONFIG[pattern_file]}" ]] && [[ -f "${CONFIG[pattern_file]}" ]]; then
# 从文件读取模式
pattern=$(paste -sd'|' "${CONFIG[pattern_file]}")
elif [[ -n "${LEVEL_PATTERNS[$severity]}" ]]; then
pattern="${LEVEL_PATTERNS[$severity]}"
else
pattern="$severity"
fi
# 查找日志文件
local log_files
log_files=$(find "$log_dir" -maxdepth 1 -name "*.log" -type f | sort -r | head -5)
if [[ -z "$log_files" ]]; then
log_error "没有找到 .log 文件"
exit 1
fi
# 清点最新活动
local total_errors=0
local total_files=0
local alert_files=()
while IFS= read -r lf; do
if [[ ! -f "$lf" ]]; then
continue
fi
local count
count=$(check_logs_in_window "$lf" "$pattern" "$window")
((total_files++))
((total_errors += count))
local fname
fname=$(basename "$lf")
local fsize
fsize=$(du -h "$lf" | cut -f1)
printf " %-30s %8s %s\n" "$fname" "$fsize" "[$count 条匹配]"
if (( count > threshold )); then
alert_files+=("$fname($count)")
fi
# 显示详细错误
if (( count > 0 )); then
show_error_details "$lf" "$pattern"
fi
done <<< "$log_files"
# 输出汇总
echo ""
echo "━━━ 汇总 ━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 检查文件数:$total_files"
echo " 匹配总数:$total_errors"
echo " 时间窗口:最近 ${window} 秒"
# 判断是否触发告警
if (( total_errors > threshold )); then
log_warn "错误数 $total_errors 超过阈值 $threshold——触发告警!"
local alert_msg
alert_msg=$(printf " 超限文件:%s\n" "$(IFS=,; echo "${alert_files[*]}")")
send_alert "[监控告警] $severity 级别错误 $total_errors 次($HOSTNAME)" \
"时间:$(date '+%Y-%m-%d %H:%M:%S')\n目录:$log_dir\n错误数:$total_errors\n$alert_msg"
else
log_info "状态正常($total_errors / $threshold)"
fi
# 退出码
if (( total_errors > threshold )); then
return 2 # 告警退出码
elif (( total_errors > 0 )); then
return 1 # 有错误但未超阈值
else
return 0 # 一切正常
fi
}
main "$@"
案例知识融合:这个脚本覆盖了本章全部核心技术——基础搜索(grep -c 计数、grep -n 行号)、扩展正则 -E([0-9]{4}-[0-9]{2}-[0-9]{2} 日期匹配、LEVEL_PATTERNS 预定义正则)、上下文 -C(show_error_details 显示前后行)、-o 与 -c(尾行快速计数)、多文件处理(find + while read 遍历日志文件)、管道组合(du -h + cut 获取文件大小)、同时使用了上一章的 关联数组 和 Here Document。
# 4.5 新手陷阱 Top 5
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | -E 遗漏——元字符被当文本 | grep "(error|fatal)" 搜索的是字面量 (error|fatal)——加 -E 才表示或 |
| 2 | 正则 . 误匹配一切 | grep "1.2.3.4" 匹配了 1a2b3c4——. 是任意字符!IP 搜索要转义 \. |
| 3 | * 含义误解 | grep "error*" 匹配 erro + 任意个 r——不是"任意字符"!.* 才是"任意" |
| 4 | -c 统计少算 | grep -c "ERROR" 统计的是匹配行数——一行 3 个 ERROR 只计 1。计数用 -o \| wc -l |
| 5 | 大文件无限制搜索 | grep "pattern" huge_file.sql 会消耗大量内存——用 --max-count 或 head 限制 |
陷阱 1 详解——-E 的缺失:
# ❌ 没加 -E——() 和 | 被当作字面量
grep "(ERROR|FATAL)" app.log
# 实际搜索的是文本 "(ERROR|FATAL)"——不是 ERROR 或 FATAL!
# ✅ 加 -E——() 和 | 获得正则含义
grep -E "ERROR|FATAL" app.log # 搜索 ERROR 或 FATAL
grep -E "(ERROR|FATAL)" app.log # 同上
# ✅ 或者用 \-E 和多个 -e
grep -e "ERROR" -e "FATAL" app.log # 等价
陷阱 3 详解——* 和 .* 的区别:
# ❌ 错误:
# grep "error*"——让 * 作用于 r
# 含义:erro + 0 或多个 r
# 匹配:erro, error, errorr, errorrr...
# ✅ 正确:匹配 "error" 后再跟任意内容
grep "error.*" app.log # .* 才是"任意数量任意字符"
grep "error" app.log # 匹配 error——最简单
# 常见误区总结:
# 错误 "error*" → 匹配 erro, error, errorr(字面理解反直觉)
# 正确 "error.*" → 匹配 error, error123, error:timeout
# 正确 "error" → 只匹配 error(精确)
# 正确 "err[aeiou]r" → 匹配 error, error...不,匹配 errar, errec, errid...
陷阱 5 详解——大文件优化:
#!/bin/bash
# ❌ 大文件无限制——会卡住
grep "SELECT" huge_dump.sql # 500MB 文件——等吧
# ✅ 优化方案 1:先定位范围
grep -n "SELECT" huge_dump.sql | head -20 # 只看前 20 个
grep --max-count=20 "SELECT" huge_dump.sql # 找到 20 个就停
# ✅ 优化方案 2:用 LC_ALL 加速
LC_ALL=C grep "SELECT" huge_dump.sql # C locale——不用处理多字节,快 2-5 倍
# ✅ 优化方案 3:分块并行(大文件专用)
split -n l/4 huge_dump.sql chunk_ # 分成 4 块
for f in chunk_*; do grep "SELECT" "$f" > "$f.out" & done
wait
cat chunk_*.out > results.txt && rm chunk_*
# ✅ 优化方案 4:用更快的工具
# 对于 1GB+ 的文件,考虑用 ripgrep(rg)替代 grep
# rg "pattern" huge_file.sql # 比 grep 快 5-10 倍
# 4.6 综合思考题
grep为什么不直接支持+和?? ——Ken Thompson 在 1974 年实现 grep 时,只实现了ed编辑器中的基础正则(*和[])。+和?是后来才加入扩展正则的。当时 Ken 是怎么选择正则特性的?如果你今天重新设计 grep,会让-E成为默认模式吗?grep -F(fgrep)的性能优势 ——固定字符串搜索不需要编译正则状态机,grep -F使用 Boyer-Moore 算法。在搜索纯文本 "localhost" 时,grep -F比grep快多少(可以自己用time命令测试)?为什么-F在某些场景下甚至比grep "localhost"快 2-3 倍?--binary-files=textvs-avsstrings——在二进制文件中搜索文本,grep -a可能匹配到不可打印的控制字符导致误报。什么时候应该先用strings binary_file | grep pattern再搜索?这个处理流程的缺陷是什么?grep和其他工具的记忆取舍 ——grep的正则、-A/-B/-C、-o、-l等选项用了就忘不了。相反,sed的保持空间、awk的NR/NF等选项长期不用就很难记住。这种"记忆差距"对 Shell 脚本的可维护性意味着什么?"一个用复杂正则链替代 Python 脚本"的 grep 一行命令,是否真的比一个简单可读的 Python 脚本更好?ripgrep(rg)对grep的取代 ——grep 诞生于 1974 年,rg 诞生于 2016 年。rg 默认递归、默认忽略.gitignore、默认彩色输出、在大型代码库上比 grep 快 5-10 倍。在什么情况下你仍然应该选择grep而不是rg?(提示:不可安装 rg 的生产环境、POSIX 兼容性要求、Docker 最小镜像)