第 4 章 · 文本处理:grep、sed、awk
本章目标:用 grep 在 api-go-demo 日志中定位错误;sed 做安全替换(先备份);awk 按列统计 slug 与状态码;用管道组合完成一次完整线上故障排查演练。
学时建议:3.5~4.5 小时(含 2.5 小时跟练)
前置:完成 linux-shell ch03(权限、重定向);~/dev-workspace 已创建。
4.1 场景说明:api-go-demo 突然 500,运营在催
周五下午,虚构商城后台 https://api.example.com/admin 商品列表接口返回 500。运维在 xiaozi-cloud 节点上拿到 api-go-demo 的访问日志与错误日志,需要在不打开 IDE 的情况下快速回答三个问题:
| 问题 | 需要的能力 |
|---|---|
| 最近 10 分钟有多少条 ERROR? | grep + wc |
哪个 slug 报错最多? | awk 按字段统计 |
| 能否在本地日志里复现并临时脱敏? | sed 替换 + 备份 |
本章在 ~/dev-workspace/logs/api.log 用教学样本模拟上述场景。样本字段与 gin-web 结构化日志、request_id 串联规则一致,便于后续 git-collab 提交 fix 后对照验证。
运营告警 → grep ERROR → awk 按 slug 聚合 → sed 脱敏测试数据 → 开发本地复现
4.2 学完你能
| 能力 | 验收标准 |
|---|---|
| grep 过滤 | 能从 500 行日志中 3 秒内筛出 ERROR |
| grep 选项 | 能解释 -n/-c/-E/-i/-R/-v/-A/-B 各自用途 |
| sed 安全改文件 | 改前 cp 或 sed -i.bak,改后能 diff 验证 |
| awk 列统计 | 能按 status= 字段计数,能写 END 汇总 |
| 管道思维 | 能画出「grep → awk → sort → uniq -c」数据流 |
| 故障排查 | 能按 request_id 串联同一请求的 INFO 与 ERROR 行 |
4.3 准备练习日志与目录结构
跟练前确认工作区(与 quick-start、git-collab 一致):
mkdir -p ~/dev-workspace/{logs,linux-notes,shop-demo/configs}
cd ~/dev-workspace
ls -la
创建本章专用 api.log(约 20 行,含 INFO/WARN/ERROR、多种 slug 与 status):
cat > ~/dev-workspace/logs/api.log << 'EOF'
2026-08-20T10:01:00.123+08:00 INFO request_id=req-1001 method=GET path=/api/products slug=usb-c-cable status=200 latency_ms=12 user_id=101
2026-08-20T10:01:01.456+08:00 INFO request_id=req-1002 method=GET path=/api/products slug=shop-mug status=200 latency_ms=18 user_id=102
2026-08-20T10:01:02.789+08:00 ERROR request_id=req-1003 method=POST path=/api/orders slug=shop-mug status=500 latency_ms=502 user_id=101 msg="db timeout after 500ms"
2026-08-20T10:01:03.012+08:00 INFO request_id=req-1004 method=GET path=/api/products slug=mech-keyboard status=200 latency_ms=9 user_id=103
2026-08-20T10:01:04.345+08:00 WARN request_id=req-1005 method=GET path=/api/products slug=old-item status=404 latency_ms=3 user_id=104
2026-08-20T10:01:05.678+08:00 ERROR request_id=req-1006 method=POST path=/api/orders slug=shop-mug status=500 latency_ms=120 user_id=105 msg="connection reset by peer"
2026-08-20T10:01:06.901+08:00 INFO request_id=req-1007 method=GET path=/healthz status=200 latency_ms=1
2026-08-20T10:01:07.234+08:00 ERROR request_id=req-1008 method=GET path=/api/products slug=kotlin-handbook status=500 latency_ms=88 user_id=106 msg="panic: nil pointer"
2026-08-20T10:01:08.567+08:00 INFO request_id=req-1009 method=GET path=/api/products slug=usb-c-cable status=200 latency_ms=11 user_id=101
2026-08-20T10:01:09.890+08:00 ERROR request_id=req-1010 method=POST path=/api/orders slug=shop-mug status=500 latency_ms=501 user_id=101 msg="db timeout after 500ms"
2026-08-20T10:01:10.123+08:00 INFO request_id=req-1011 method=GET path=/api/products slug=spring-boot-guide status=200 latency_ms=15 user_id=107
2026-08-20T10:01:11.456+08:00 WARN request_id=req-1012 method=GET path=/api/products slug=deprecated-sku status=404 latency_ms=2 user_id=108
2026-08-20T10:01:12.789+08:00 ERROR request_id=req-1013 method=GET path=/api/admin/products status=403 latency_ms=5 user_id=109 msg="forbidden: missing role ops"
2026-08-20T10:01:13.012+08:00 INFO request_id=req-1014 method=GET path=/api/products slug=shop-mug status=200 latency_ms=14 user_id=102
2026-08-20T10:01:14.345+08:00 ERROR request_id=req-1015 method=POST path=/api/orders slug=mech-keyboard status=500 latency_ms=200 user_id=103 msg="inventory lock failed"
2026-08-20T10:01:15.678+08:00 INFO request_id=req-1016 method=GET path=/api/products slug=usb-c-cable status=200 latency_ms=10 user_id=101
2026-08-20T10:01:16.901+08:00 ERROR request_id=req-1017 method=POST path=/api/orders slug=shop-mug status=500 latency_ms=499 user_id=105 msg="db timeout after 500ms"
2026-08-20T10:01:17.234+08:00 INFO request_id=req-1018 method=GET path=/metrics status=200 latency_ms=2
2026-08-20T10:01:18.567+08:00 WARN request_id=req-1019 method=GET path=/api/products slug= status=400 latency_ms=1 user_id=110 msg="missing slug param"
2026-08-20T10:01:19.890+08:00 ERROR request_id=req-1020 method=GET path=/api/products slug=kotlin-handbook status=500 latency_ms=91 user_id=106 msg="panic: nil pointer"
EOF
验收(步骤 1):
| 检查项 | 命令 | 期望 |
|---|---|---|
| 文件存在 | wc -l ~/dev-workspace/logs/api.log | 20 |
| 含 ERROR | grep -c ERROR ~/dev-workspace/logs/api.log | 8 |
| 含 shop-mug | grep -c shop-mug ~/dev-workspace/logs/api.log | ≥ 6 |
读懂输出:wc -l 统计行数;-l 来自 line。若显示 0 行,多半是路径写错或 heredoc 未正确结束(缺少单独一行的 EOF)。
为什么用固定样本而非直接读 /var/log:教学环境权限不一;虚构字段与 shop_db 商品 slug(usb-c-cable、shop-mug、mech-keyboard)对齐,后续 go-database 报表可对照。
4.4 grep 基础:从日志里「捞针」
grep(Global Regular Expression Print)按模式逐行扫描文件或标准输入,匹配则打印整行。
4.4.1 跟练:第一次 grep
cd ~/dev-workspace
grep ERROR logs/api.log
期望输出(节选,共 8 行):
2026-08-20T10:01:02.789+08:00 ERROR request_id=req-1003 method=POST path=/api/orders slug=shop-mug status=500 ...
2026-08-20T10:01:05.678+08:00 ERROR request_id=req-1006 method=POST path=/api/orders slug=shop-mug status=500 ...
...
读懂输出:每行以 ISO8601 时间戳开头;第二列是级别 ERROR;后续是 key=value 结构化字段。grep 默认大小写敏感,只显示匹配行,不修改文件。
为什么先看 ERROR 再看 WARN:500 类故障通常对应 ERROR;WARN(404/400)可能是业务预期,排查时优先级较低。
4.4.2 跟练:行号、计数、反选
grep -n ERROR logs/api.log | head -3
grep -c ERROR logs/api.log
grep -c INFO logs/api.log
grep -v ERROR logs/api.log | grep -v INFO | head -5
验收(步骤 2):
| 命令 | 期望结果 | |
|---|---|---|
| `grep -n ERROR logs/api.log \ | head -1` | 行号 3: 开头(第 3 行第一条 ERROR) |
grep -c ERROR logs/api.log | 8 | |
grep -c INFO logs/api.log | 8 | |
| 最后一管道 | mainly WARN 行 |
读懂输出:-n 前缀 3: 表示匹配发生在源文件第 3 行,便于 sed -n '3p' 或编辑器跳转;-c 只输出数字;-v 反选(invert),配合第二次 grep 可筛出 WARN。
为什么 -n 在多人协作时极其重要:你把行号贴到 git-collab Issue,开发可直接定位,比截图更高效。
4.5 grep 选项详解(生产常用)
| 选项 | 含义 | 示例场景 | ||
|---|---|---|---|---|
-i | 忽略大小写 | 日志级别写成 error 或 Error | ||
-n | 显示行号 | 报 bug 时引用 | ||
-c | 只输出匹配行数 | 监控脚本阈值 | ||
-v | 反选不匹配行 | 排除健康检查 /healthz | ||
-E | 扩展正则(`\ | ` 或) | `ERROR\ | WARN` |
-F | 固定字符串(非正则) | 搜索含 . 的域名 | ||
-R | 递归目录 | 在 shop-demo 搜 TODO | ||
-w | 整词匹配 | 避免 log 匹配 catalog | ||
-A N | 匹配行及后 N 行 | 看 ERROR 后的 stack | ||
-B N | 匹配行及前 N 行 | 看 ERROR 前的 request 上下文 | ||
-C N | 前后各 N 行 | 等同 -A N -B N | ||
--include | 递归时过滤扩展名 | 只搜 *.go | ||
--exclude-dir | 跳过目录 | 跳过 .git node_modules |
4.5.1 跟练:扩展正则与上下文
grep -E 'ERROR|WARN' logs/api.log | wc -l
grep -E 'status=(500|403)' logs/api.log
grep -A1 'db timeout' logs/api.log
grep -B1 'req-1003' logs/api.log
grep '/healthz' logs/api.log
grep -v '/healthz' logs/api.log | grep -v '/metrics' | wc -l
期望:-E 'ERROR|WARN' 共 12 行(8 ERROR + 4 WARN);-A1 'db timeout' 每个匹配块多一行后续内容(本样本后续无 stack,可能仅多一行或空)。
为什么 排除 /healthz 与 /metrics:K8s 探针与 Prometheus scrape 会产生大量「正常」INFO,干扰 ERROR 占比统计。
4.5.2 跟练:按 slug 与 request_id 精查
grep 'slug=shop-mug' logs/api.log
grep 'slug=shop-mug' logs/api.log | grep ERROR
grep 'request_id=req-1003' logs/api.log
grep -E 'slug=(usb-c-cable|shop-mug)' logs/api.log | grep -c status=200
验收(步骤 3):
| 命令 | 期望 |
|---|---|
| shop-mug 的 ERROR 行数 | 4(req-1003/1006/1010/1017) |
| req-1003 仅 1 行 | 对应 POST orders 500 |
| usb-c 或 shop-mug 且 200 | 5 |
读懂输出:同一 slug 既有 200 也有 500,说明不是「商品不存在」,而是写操作(POST orders)或依赖服务(DB)问题——这与 go-database 连接池超时的故事线一致。
4.5.3 跟练:递归搜索 shop-demo(模拟搜密钥泄露)
先在项目中放几个教学文件:
mkdir -p ~/dev-workspace/shop-demo/{cmd/api,configs}
cat > ~/dev-workspace/shop-demo/configs/app.yaml << 'EOF'
server:
port: 8080
log_level: info
# TODO: rotate db password quarterly
EOF
cat > ~/dev-workspace/shop-demo/cmd/api/main.go << 'EOF'
package main
// demo only — never commit real secrets
const demoToken = "sk-demo-not-real"
func main() {}
EOF
递归搜索(排除 .git):
grep -Rni 'password\|secret\|sk-' ~/dev-workspace/shop-demo 2>/dev/null
grep -Rni --include='*.go' 'TODO' ~/dev-workspace/shop-demo
grep -Rni --exclude-dir=.git 'demo' ~/dev-workspace/shop-demo | head
为什么 2>/dev/null:递归时可能遇到无权限目录,stderr 噪音会淹没结果;生产应用 --include 缩小范围以控时延。
4.6 grep 与管道:只看「最新」错误
真实环境日志可能 GB 级,不会 cat 全文件。组合 tail 与 grep:
tail -5 logs/api.log
tail -5 logs/api.log | grep ERROR
cat logs/api.log | grep ERROR | tail -3
cat logs/api.log | grep 'shop-mug' | grep ERROR | wc -l
期望最后一条:4(与 4.5.2 一致)。
数据流图:
api.log ──cat──► grep ERROR ──► tail -3 ──► 终端(最近 3 条 ERROR)
└──tail -5──► grep ERROR ──► 最近 5 行内的 ERROR
为什么 管道从左到右:每个命令读上游 stdout,写自身 stdout;grep 不缓冲整个文件到内存(按行流式),适合大日志。
4.7 sed:流编辑器与安全替换
sed 按规则逐行变换文本。默认只输出到 stdout,不改源文件——这是新手最该记住的安全绳。
4.7.1 跟练:预览替换(不改文件)
sed 's/status=500/status=503/g' logs/api.log | grep 503 | head -3
grep -c 'status=500' logs/api.log
grep -c 'status=500' <(sed 's/status=500/status=503/g' logs/api.log)
期望:原文件 status=500 仍为 8 处(或你的 ERROR 中带 500 的行数);进程替换 <(...) 内 sed 输出中 500 变为 503。
读懂输出:s/旧/新/g 中 g 表示一行内全局替换;无 g 时只换每行第一个匹配。
为什么 先预览再 -i:生产误改 nginx 或 env 可能导致全站 502;学院约定:改配置必备份(与 ops-deploy 变更流程一致)。
4.7.2 跟练:删除行与注释行
sed '/WARN/d' logs/api.log | grep -c WARN
sed '/^2026.*INFO.*healthz/d' logs/api.log | grep healthz
sed -n '1,3p' logs/api.log
期望:/WARN/d 后 WARN 计数为 0;-n '1,3p' 只打印第 1~3 行(-n 默认不自动打印)。
4.7.3 跟练:原地修改与备份(-i.bak)
cp logs/api.log logs/api.log.bak.$(date +%Y%m%d%H%M%S)
sed -i.bak 's/shop-mug/shop-cup/g' logs/api.log
grep -c shop-cup logs/api.log
grep -c shop-mug logs/api.log