下载工作台
Linux 与 Shell 入门

文本处理:grep、sed、awk

试读上半部分 · 解锁后可读全文

第 4 章 · 文本处理:grep、sed、awk

本章目标:用 grepapi-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 安全改文件改前 cpsed -i.bak,改后能 diff 验证
awk 列统计能按 status= 字段计数,能写 END 汇总
管道思维能画出「grep → awk → sort → uniq -c」数据流
故障排查能按 request_id 串联同一请求的 INFO 与 ERROR 行

4.3 准备练习日志与目录结构

跟练前确认工作区(与 quick-startgit-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.log20
含 ERRORgrep -c ERROR ~/dev-workspace/logs/api.log8
含 shop-muggrep -c shop-mug ~/dev-workspace/logs/api.log≥ 6

读懂输出wc -l 统计行数;-l 来自 line。若显示 0 行,多半是路径写错或 heredoc 未正确结束(缺少单独一行的 EOF)。

为什么用固定样本而非直接读 /var/log:教学环境权限不一;虚构字段与 shop_db 商品 slug(usb-c-cableshop-mugmech-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.log8
grep -c INFO logs/api.log8
最后一管道mainly WARN 行

读懂输出-n 前缀 3: 表示匹配发生在源文件第 3 行,便于 sed -n '3p' 或编辑器跳转;-c 只输出数字;-v 反选(invert),配合第二次 grep 可筛出 WARN。

为什么 -n 在多人协作时极其重要:你把行号贴到 git-collab Issue,开发可直接定位,比截图更高效。


4.5 grep 选项详解(生产常用)

选项含义示例场景
-i忽略大小写日志级别写成 errorError
-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 且 2005

读懂输出:同一 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/旧/新/gg 表示一行内全局替换;无 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

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。