Appearance
grep 与 sed
运维天天跟文本打交道——翻日志找错误、批量改配置、过滤命令输出。grep 负责匹配查找,sed 负责流式编辑,这两个是 Shell 里用得最多的文本处理工具。
一、grep 定位
grep(Global Regular Expression Print)从文本输入中筛选匹配指定模式的行。它可以读取文件,也接受管道输入:
bash
grep "ERROR" app.log
journalctl -u nginx --since "1 hour ago" | grep "failed"常用参数:
| 参数 | 作用 |
|---|---|
-i | 忽略大小写 |
-n | 显示匹配行的行号 |
-v | 反向匹配——输出不包含匹配内容的行 |
-r | 递归搜索目录下的所有文件 |
-E | 使用扩展正则表达式(支持 +、?、| 等) |
-F | 按普通字符串匹配(不解释正则特殊字符) |
-q | 静默模式,不输出任何内容,只看退出码 |
-A N | 显示匹配行后面 N 行 |
-B N | 显示匹配行前面 N 行 |
-C N | 显示匹配行前后各 N 行 |
查错误并显示行号:
bash
grep -n "ERROR" app.log查看错误上下文(前后 3 行):
bash
grep -C 3 "OutOfMemory" app.log脚本中只判断是否存在匹配而不输出内容:
bash
if grep -q "ERROR" app.log; then
echo "found error"
fi二、正则与字符串匹配
不写 -E 时,grep 默认使用 BRE(Basic Regular Expression,基本正则表达式)。在 BRE 模式下,+、?、|、() 等字符需要用反斜杠转义才能获得特殊含义——grep "ERROR\|WARN" 而非 grep "ERROR|WARN"。习惯用 grep -E(ERE,扩展正则)可以省掉这些反斜杠,写法和大多数编程语言的正则更接近。
很多场景下只是查找固定的字符串,用 -F 更直接——它不解释正则特殊字符:
bash
grep -F "10.0.0.1" access.log
grep -F "[ERROR]" app.log[、.、* 等字符在正则中有特殊含义,用 -F 之后就不需要逐个转义。如果用普通 grep(不加 -F)搜索含有这些字符的字符串,匹配结果可能不符合预期——这是个隐蔽的坑。
扩展正则用 -E:
bash
grep -E "ERROR|WARN|FATAL" app.log常用正则语法(配合 -E):
| 写法 | 含义 |
|---|---|
^ | 行首 |
$ | 行尾 |
. | 匹配任意单个字符 |
* | 前一字符重复 0 次或多次 |
+ | 前一字符重复 1 次或多次(需要 -E) |
? | 前一字符出现 0 或 1 次(需要 -E) |
[] | 字符集合,[0-9] 匹配一位数字 |
| | 或,多个模式取其一(需要 -E) |
粗略匹配 IPv4 地址:
bash
grep -E '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' access.log这个正则只按格式过滤,不校验每段是否在 0–255 范围内。日志临筛够用,严格校验时需要更精确的工具。
三、grep 退出码
grep 的退出码含义清晰,适合写入条件逻辑:0 是至少找到一处匹配,1 是未找到任何匹配,2 是发生错误(文件不存在、目录无法读取等)。
在 set -e 脚本中处理 grep 无匹配的返回值:
bash
grep "ERROR" app.log || true # 允许 "没有匹配到" 不算致命错误|| true 只应在确实允许为空的地方使用。盲目加会连真正的错误(如文件不存在、退出码 2)也吞掉。更稳妥的做法是用 if 包裹,显式区分"没匹配到"和"出错了"。
四、sed 定位
sed(Stream Editor)按行读取、按指令处理、再输出结果。默认不改动原文件,结果打印到标准输出——这点跟很多人预期不一样,以为 sed 改了文件其实没有。
打印指定行范围:
bash
sed -n '1,5p' app.log # 只输出第 1 到 5 行删除空行:
bash
sed '/^$/d' app.log # 删除匹配行,输出到屏幕,不修改原文件替换文本:
bash
sed 's/old/new/' app.conf # 替换每行第一个匹配
sed 's/old/new/g' app.conf # 替换每行所有匹配(g = global)s/old/new/ 只影响每行的第一个匹配,末尾加 g 才会替换整行中所有出现的 old——新手常忘加 g,结果发现只换了第一个。
五、sed 行地址
sed 命令前可以加"地址"来限制只在匹配的行上执行操作:
| 地址写法 | 含义 |
|---|---|
1p | 第 1 行 |
1,10p | 第 1 到 10 行 |
/ERROR/p | 包含 ERROR 的行 |
$p | 最后一行 |
只看第 10 到 20 行:
bash
sed -n '10,20p' app.log只在匹配行上执行替换:
bash
sed '/server_name/s/example.com/api.example.com/' nginx.conf这个写法先找到包含 server_name 的行,再在这行里把 example.com 替换成 api.example.com。sed 不识别配置文件的结构语义——它只做纯文本匹配。多层缩进的 YAML、JSON 配置不适合用 sed 深度修改,容易改坏。
六、原地修改
Linux 上 sed 的 -i 参数支持"原地修改"文件(而不是输出到屏幕):
bash
sed -i.bak 's/listen 80/listen 8080/' nginx.conf-i.bak 表示修改原文件前,先备份为 nginx.conf.bak。裸用 sed -i 不留备份,生产线操作风险较高——改错了没法回滚。
批量替换目录中的多个文件:
bash
find /etc/nginx -type f -name "*.conf" -print0 |
while IFS= read -r -d '' file; do
sed -i.bak 's/old.example.com/new.example.com/g' "$file"
done批量修改配置后,马上跑服务自带的语法检查(如 nginx -t、sshd -t)确认没有语法错误再继续。别等重启服务报错了才发现配置改坏。
七、文本处理模式
去掉注释行和空白行:
bash
grep -v '^[[:space:]]*#' app.conf | grep -v '^[[:space:]]*$'[[:space:]] 是 POSIX 字符类,匹配空格和 Tab,比 \s 跨平台兼容性更好。
替换整行配置(匹配到行后整行覆盖):
bash
sed -i.bak 's/^worker_processes.*/worker_processes auto;/' nginx.conf在匹配行后面追加一行内容:
bash
sed '/http {/a\ server_tokens off;' nginx.conf多个 sed 操作用分号分隔,也可以写多个 -e。逻辑变复杂时写成独立的 sed 脚本文件,比一行长命令更便于维护和回滚——一行流 sed 看着帅,过两个月自己都看不懂。
八、使用边界
grep 和 sed 适合的场景:从日志中过滤关键字用 grep、查看匹配行的上下文用 grep -C、简单替换一行配置用 sed、批量删除注释和空行用 sed/grep、批量文本替换用 sed -i.bak。
不适合的场景要知道换工具:修改 JSON 结构用 jq、修改 YAML 配置用 yq 或配置管理工具、解析多层嵌套的日志字段用 awk/Python/日志平台、涉及上下文状态的复杂文本处理用 Python/Go 等通用语言。
grep 和 sed 是单行处理工具,不关心上下文结构。当需要理解文件语法(缩进、嵌套、引用关系)时,用能解析该格式的专用工具会更准确——硬用 sed 改 JSON,改出语法错误是常态。