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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
🔑 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 避免转义地狱
2
3
4
5
6
# 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 # 标点符号
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
🔑 正则元字符速查表:
| 元字符 | 含义 | 示例 |
|---|---|---|
. | 任意单个字符 | 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"(. 被转义为字面量)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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 # 不输出错误——适合在脚本中使用
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
🔑 常用选项速查表:
| 选项 | 含义 | 场景 |
|---|---|---|
-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
2
3
4
5
6
7
8
# 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"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
📌 新写 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"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
🔑 单词边界实用模式:
# 搜索完整单词 "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)
2
3
4
5
6
7
8
9
10
11
# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
🔑 后向引用实际应用:
# ===== 查找 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=张三,再提取最后的单词)
2
3
4
5
6
7
8
9
10
# 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="──────────"
# 不同匹配组之间用 ────────── 分隔
2
3
4
5
6
7
8
9
10
11
12
13
14
🔑 实战:分析堆栈跟踪:
# 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 语句及其错误详情
2
3
4
5
6
7
# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 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 读取模式
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 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 个
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 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"
# 只显示真实的错误,排除已知的安全警告
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 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 的地方
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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 丢弃之前匹配的内容,(?=...) 向前查找——只提取标题文本
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 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 "$@"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
案例知识融合:这个脚本覆盖了本章全部核心技术——基础搜索(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 # 等价
2
3
4
5
6
7
8
9
10
陷阱 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...
2
3
4
5
6
7
8
9
10
11
12
13
14
陷阱 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 倍
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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 最小镜像)