Skip to content

grep 命令使用详解 ​

简介 ​

grep 是 Unix/Linux 中最常用的文本搜索工具,名字来自 ed 编辑器的命令 g/re/p,即 Global Regular Expression Print:用正则表达式在输入中逐行查找,打印匹配的行。

它最初由 Ken Thompson 在贝尔实验室开发,后来成为 POSIX 标准的一部分。今天 Linux 上常见的实现是 GNU grep,BSD/macOS 上则是 BSD grep;功能更强、速度更快的 ripgrep(命令名 rg)也很流行,但 grep 仍然是脚本和日常排障的基础工具。

文本处理三剑客

本文是系列文章之一:grep 负责按行筛选,sed 负责流式编辑,awk 负责字段处理与统计。

核心特点 ​

  • 按行匹配:grep 一次处理一行,默认输出整行
  • 支持正则:BRE、ERE,GNU grep 还支持 PCRE
  • 组合灵活:可与管道、xargs、sed、awk 组合
  • 退出状态明确:匹配、未匹配、出错分别返回 0、1、2,适合脚本判断
  • 流式处理:不需要把整个文件读入内存

grep 只负责“找行”,不负责改文件、取字段或做统计。批量替换用 sed,按字段提取和聚合用 awk。

安装与版本 ​

大多数 Linux 发行版已经预装:

bash
sudo apt update
sudo apt install grep
bash
sudo dnf install grep
bash
sudo pacman -S grep
bash
apk add grep

查看版本:

bash
grep --version
# grep (GNU grep) 3.12

macOS 自带的是 BSD grep,功能选项和 GNU grep 略有差异。需要 GNU 行为时可以安装:

bash
brew install grep
# Homebrew 通常会安装为 ggrep,避免覆盖系统 grep
ggrep --version

egrep 和 fgrep 已过时

egrep 等价于 grep -E,fgrep 等价于 grep -F。这两个命令在新版 GNU grep 中已经不推荐使用,脚本里应直接写 grep -E 或 grep -F。

基本语法 ​

bash
grep [选项] 模式 [文件...]

常见形式:

bash
# 在文件中搜索
grep 'error' app.log

# 从标准输入搜索
cat app.log | grep 'error'

# 多个文件
grep 'error' app.log access.log

# 递归搜索目录
grep -r 'error' /var/log

# 使用扩展正则
grep -E 'error|warning' app.log

# 按字面量搜索,不解释正则元字符
grep -F 'a.b[c]' file.txt

示例数据 ​

后面的例子主要使用这份 demo.txt:

bash
cat > demo.txt <<'EOF'
apple 10 red
banana 5 yellow
Cherry 20 red
date 15 brown
error: disk full
warning: memory low
EOF
bash
grep 'red' demo.txt

输出:

text
apple 10 red
Cherry 20 red

grep 默认输出包含匹配内容的整行,而不是只输出匹配到的片段。只输出匹配部分需要 -o。

三种正则模式 ​

选项模式说明
默认BRE(基本正则)POSIX 基本正则,+、?、() 需要转义
-EERE(扩展正则)支持 +、?、()、{m,n},推荐日常使用
-F固定字符串不解释任何正则元字符,速度最快
-PPCREGNU grep 的 Perl 兼容正则,支持 \d、环视等

BRE 中的「或」要写成 \|,ERE 中直接写 |:

bash
# BRE
printf 'error\nwarning\n' | grep 'error\|warning'

# ERE
printf 'error\nwarning\n' | grep -E 'error|warning'

常用选项 ​

匹配控制 ​

选项说明示例
-i忽略大小写grep -i 'cherry' demo.txt
-v反向选择,输出不匹配的行grep -v 'red' demo.txt
-w只匹配完整单词grep -w 'cat' file.txt
-x只匹配整行grep -x 'error: disk full' demo.txt
-F按固定字符串匹配grep -F 'a.b[c]' file.txt
-E使用扩展正则grep -E 'error|warning' demo.txt
-P使用 PCRE,GNU grepgrep -P '\d+' file.txt
-e指定一个模式,可重复grep -e red -e yellow demo.txt
-f从文件读取模式,一行一个grep -f patterns.txt demo.txt
-m N每个文件最多匹配 N 行后停止grep -m1 'ERROR' app.log

输出控制 ​

选项说明示例
-n显示行号grep -n 'red' demo.txt
-H强制显示文件名grep -H 'red' demo.txt
-h不显示文件名grep -h 'red' *.txt
-o只输出匹配部分grep -o 'red' demo.txt
-c统计匹配的行数grep -c 'red' demo.txt
-l只列出包含匹配的文件名grep -l 'error' *.log
-L只列出不包含匹配的文件名grep -L 'error' *.log
-q静默模式,不输出,靠退出状态判断grep -q 'error' app.log
-s抑制文件不存在、权限不足等错误grep -s 'pattern' /root/file
-b显示匹配所在的字节偏移grep -b 'red' demo.txt
-A N显示匹配行之后的 N 行grep -A2 'panic' syslog
-B N显示匹配行之前的 N 行grep -B2 'panic' syslog
-C N显示匹配行前后各 N 行grep -C2 'panic' syslog
-z输入按 NUL 分隔,而不是换行grep -z 'pattern' file
-Z输出文件名后加 NUL,配合 xargs -0grep -lZ 'pattern' .
--line-buffered行缓冲输出,适合管道实时处理tail -f app.log | grep --line-buffered ERROR

文件与目录 ​

选项说明示例
-r递归搜索目录,只跟随命令行上的符号链接grep -r 'error' /var/log
-R递归搜索目录,跟随所有符号链接grep -R 'error' /var/log
--include只搜索匹配 glob 的文件grep -rn --include='*.log' 'ERROR' /var/log
--exclude排除匹配 glob 的文件grep -rn --exclude='*.min.js' 'debug' .
--exclude-dir排除目录grep -rn --exclude-dir=.git 'TODO' .
-a把二进制文件当文本处理grep -a 'pattern' binary
-I忽略二进制文件grep -I 'pattern' *

正则表达式 ​

BRE 与 ERE 的区别 ​

这是 grep 最常见的坑。默认的 BRE 中,+、?、|、(、)、{、} 都不是特殊字符,需要使用 \+、\?、\|、\(、\)、\{、\} 才有特殊含义。其中 \+、\?、\| 是 GNU 扩展,\(、\)、\{、\} 是 POSIX BRE 语法。-E 打开的 ERE 则可以直接写。

bash
# BRE:匹配字面量 a+b
printf 'a+b\naaab\n' | grep 'a+b'
# a+b

# ERE:a 重复一次或多次
printf 'a+b\naaab\n' | grep -E 'a+b'
# aaab

同理:

bash
# 匹配字面量 a|b
grep 'a|b' file.txt

# 匹配 a 或 b
grep -E 'a|b' file.txt

日常使用建议优先 grep -E,可读性更好。只有在需要兼容非常老的系统或严格 POSIX 环境时,才需要手写 BRE 转义。

常用元字符 ​

写法含义示例
.任意单个字符(不含换行)grep -E 'a.c'
*前一项重复 0 次或多次grep -E 'ab*c'
+前一项重复 1 次或多次,EREgrep -E 'ab+c'
?前一项出现 0 次或 1 次,EREgrep -E 'ab?c'
{n,m}前一项重复 n 到 m 次,EREgrep -E '[0-9]{2,4}'
^行首grep '^ERROR' app.log
$行尾grep 'ERROR$' app.log
[abc]字符集合grep '[abc]'
[^abc]排除字符集合grep '[^0-9]'
(...)分组,EREgrep -E '(ab)+'
|或,EREgrep -E 'error|warning'
\转义grep 'a\.b'

POSIX 字符类 ​

字符类含义
[[:alpha:]]字母
[[:digit:]]数字
[[:alnum:]]字母和数字
[[:space:]]空白字符
[[:blank:]]空格和 Tab
[[:lower:]] / [[:upper:]]小写 / 大写字母
[[:punct:]]标点符号
[[:xdigit:]]十六进制数字
[[:cntrl:]]控制字符
[[:print:]] / [[:graph:]]可打印字符 / 可见字符
bash
# 匹配以数字开头的行
grep -E '^[[:digit:]]' file.txt

# 匹配包含空白的行
grep '[[:space:]]' file.txt

不要依赖 [a-z]

在某些 locale 下,[a-z] 的范围可能包含非 ASCII 字符,或者排序行为与预期不同。处理 ASCII 数据时可以设置 LC_ALL=C;需要按语言习惯匹配时,用 [[:lower:]]、[[:alpha:]] 等字符类。

单词边界 ​

bash
# 只匹配完整单词 cat,不匹配 concatenate
grep -w 'cat' file.txt

# GNU grep 也支持 \< 和 \>
grep '\<cat\>' file.txt

# \b 是 GNU 扩展,ERE 中也可以使用
grep -E '\bcat\b' file.txt

-w 判断的“单词字符”包括字母、数字和下划线,所以 cat 不会匹配 cat_1,但会匹配 cat.,因为 . 是边界。

分组与反向引用 ​

BRE 中分组写成 \(...\),反向引用写成 \1、\2:

bash
# 匹配重复的单词,例如 foo foo
grep '\(foo\)[[:space:]]\+\1' file.txt

# ERE 写法
grep -E '(foo)[[:space:]]+\1' file.txt

ERE 的反向引用在 POSIX 中没有强制要求,但 GNU grep 支持。需要跨实现可移植时,优先用 -E 的基础分组,或改用 awk。

PCRE 扩展 ​

GNU grep 的 -P 使用 PCRE,支持 \d、\s、\w、环视、非贪婪等:

bash
# 提取 URL 中 user= 后面的值
grep -oP '(?<=user=)[^&]+' url.txt

# 匹配三位数字加四位数字
grep -P '\d{3}-\d{4}' file.txt

# 负向前瞻:foo 后面不是 bar
grep -P 'foo(?!bar)' file.txt

-P 不是所有 grep 都支持

-P 是 GNU grep 的编译期可选功能。某些发行版或 BSD/macOS 的 grep 没有 PCRE 支持;脚本中如果需要可移植性,使用 -E,或者显式依赖 grep -P 并做好检测。

贪婪匹配 ​

grep 的正则默认是贪婪的,.* 会尽可能匹配到行尾:

bash
echo 'a1b2c' | grep -oE 'a.*c'
# a1b2c

BRE/ERE 没有非贪婪量词。要匹配“最短”的一段,通常用排除字符类:

bash
# 贪婪匹配:整段 <a><b> 被当成一个匹配
echo '<a><b>' | grep -oE '<.*>'
# <a><b>

# 排除字符类:分别匹配 <a> 和 <b>
echo '<a><b>' | grep -oE '<[^>]*>'
# <a>
# <b>

需要真正的非贪婪匹配时用 -P:

bash
echo '<a><b>' | grep -oP '<.*?>'
# <a>
# <b>

匹配模式与多模式 ​

多个模式 ​

bash
# 多个 -e
grep -e 'error' -e 'warning' app.log

# ERE 的或
grep -E 'error|warning' app.log

# 从文件读取模式
cat > patterns.txt <<'EOF'
error
warning
critical
EOF

grep -f patterns.txt app.log

-f 文件中的每一行是一个模式。空行会被当成空正则,匹配所有行;模式文件末尾的换行不会额外产生空模式,但中间的空行要特别注意。

固定字符串 ​

模式中包含 .、[、]、*、\ 等字符时,如果只想按字面量搜索,用 -F:

bash
grep -F 'a.b[c]' file.txt

相比 grep 'a\.b\[c\]',-F 更直观,速度也更快。

整词与整行 ​

bash
# 整词
grep -w 'cat' file.txt

# 整行
grep -x '127.0.0.1 localhost' /etc/hosts

-w 和 -x 都可以和 -i、-v、-r 等组合使用。

模式以短横线开头 ​

如果模式本身以 - 开头,grep 可能把它当成选项。用 -e 或 -- 明确分隔:

bash
grep -e '-foo' file.txt
grep -- '-foo' file.txt

大小写 ​

bash
grep -i 'cherry' demo.txt
# Cherry 20 red

-i 受 locale 影响。处理 ASCII 数据时,LC_ALL=C grep -i 的行为更稳定,速度也通常更快。

文件与目录搜索 ​

递归搜索 ​

bash
# 递归搜索当前目录
grep -r 'TODO' .

# 只搜索 .py 文件,排除 .git 和虚拟环境
grep -rn \
  --include='*.py' \
  --exclude-dir=.git \
  --exclude-dir=venv \
  'TODO' .

-r 和 -R 的区别:

  • -r:递归目录,只跟随命令行上显式给出的符号链接
  • -R:递归目录,跟随所有符号链接,可能陷入循环

大多数场景用 -r 就够了。

列出文件名 ​

bash
# 包含匹配的文件
grep -rl 'ERROR' /var/log

# 不包含匹配的文件
grep -rL 'ERROR' /var/log

# 文件名以 NUL 分隔,适合交给 xargs -0
grep -rlZ 'ERROR' /var/log | xargs -0 -r echo

-l 在每个文件第一次匹配后就停止读取,通常比 -n 更快。

统计行数 ​

bash
# 单个文件:匹配行数
grep -c 'ERROR' app.log

# 多个文件:每个文件一行计数
grep -c 'ERROR' app.log access.log
# app.log:3
# access.log:0

-c 统计的是匹配的行数,不是匹配次数。一行中出现两次关键字也只算 1。统计出现次数要配合 -o:

bash
grep -o 'ERROR' app.log | wc -l

二进制文件 ​

grep 默认会识别二进制文件,匹配时只提示:

text
Binary file bin.dat matches

需要把二进制当文本搜索:

bash
grep -a 'TODO' bin.dat

需要直接忽略二进制文件:

bash
grep -I 'TODO' *

-I 等价于 --binary-files=without-match。反过来,--binary-files=text 等价于 -a。

压缩文件 ​

grep 不能直接读取 .gz,可以用 zgrep:

bash
zgrep 'ERROR' app.log.gz

zgrep 本质上是对压缩文件解压后调用 grep,选项基本通用。.xz、.bz2 也有对应的 xzgrep、bzgrep。

输出控制 ​

行号、文件名与偏移 ​

bash
grep -n 'ERROR' app.log
# 12:ERROR database timeout

grep -H 'ERROR' app.log
# app.log:12:ERROR database timeout

grep -h 'ERROR' app.log access.log
# 不显示文件名

默认情况下,搜索多个文件或递归搜索时会显示文件名;搜索单个文件时不显示。-H 强制显示,-h 强制隐藏。

bash
grep -b 'ERROR' app.log
# 显示匹配行(加 -o 时是匹配片段)的字节偏移

-b 在分析大文件或二进制文件时比较有用。

只输出匹配部分 ​

bash
grep -o 'red' demo.txt
# red
# red

一行中有多个匹配时,-o 会为每个匹配输出一行:

bash
printf 'foo bar foo\n' | grep -o 'foo'
# foo
# foo

配合 sort、uniq 可以做简单的词频统计:

bash
grep -oE '[[:alpha:]]+' demo.txt | tr '[:upper:]' '[:lower:]' | sort | uniq -c | sort -rn

上下文 ​

bash
# 匹配行之后 2 行
grep -A2 'panic' syslog

# 匹配行之前 2 行
grep -B2 'panic' syslog

# 前后各 2 行
grep -C2 'panic' syslog

上下文行之间用 -- 分隔。可以自定义分隔符:

bash
grep -C2 --group-separator='...' 'panic' syslog
grep -C2 --no-group-separator 'panic' syslog

限制匹配数量 ​

bash
# 找到第一个 ERROR 就停止
grep -m1 'ERROR' app.log

# 每个文件最多输出 5 条匹配
grep -m5 'ERROR' *.log

-m 对日志排查很有用,尤其是文件很大、只关心是否出现或前几条记录时。

静默与退出状态 ​

bash
if grep -q 'ERROR' app.log; then
    echo '发现错误'
fi

-q 不输出任何内容,第一次匹配就返回 0,适合脚本判断。需要同时保留输出时不要用 -q。

颜色与管道 ​

bash
grep --color=auto 'ERROR' app.log

--color=auto 只在终端输出时着色,管道或重定向时不着色。如果希望管道下游也能看到颜色,使用 --color=always,但下游程序可能会把 ANSI 转义序列当成普通字符。

退出状态与脚本 ​

grep 的退出状态:

状态含义
0至少有一行被选中
1没有选中任何行
2发生错误(文件不存在、权限不足、正则非法等)

if 判断 ​

bash
if grep -q 'ERROR' app.log; then
    echo '日志中有错误'
else
    echo '日志正常'
fi

set -e 的坑 ​

set -e 会把退出状态 1 当成失败,所以下面这行会让脚本直接退出:

bash
set -e
grep -q 'ERROR' app.log   # 没有匹配时脚本退出

可以显式处理:

bash
if grep -q 'ERROR' app.log; then
    ...
fi

# 或者允许失败
grep -q 'ERROR' app.log || true

grep -c 的退出状态 ​

即使输出 0,没有匹配时 grep -c 的退出状态仍然是 1:

bash
count=$(grep -c 'ERROR' app.log || true)
echo "$count"

管道中的状态 ​

bash
grep 'ERROR' app.log | grep -v 'DEBUG'
echo "${PIPESTATUS[@]}"

在 Bash 中,PIPESTATUS 保存管道中每个命令的退出状态。需要严格判断时,不要只看最后一个命令的 $?。

grep -q 与 SIGPIPE

grep -q 匹配后立即退出,如果上游还在大量输出(例如 yes、tail -f),上游可能收到 SIGPIPE。开启 set -o pipefail 时,管道整体状态可能变成 141。脚本里可以用输入重定向代替管道:grep -q pattern < file。

实战案例 ​

日志检索 ​

bash
# 查找错误并显示上下文
grep -n -C2 'ERROR' app.log

# 同时匹配多个级别
grep -E 'ERROR|WARN|FATAL' app.log

# 排除调试日志
grep -v 'DEBUG' app.log | grep 'ERROR'

# 只看前 3 条
grep -m3 'ERROR' app.log

# 实时跟踪并过滤
tail -f app.log | grep --line-buffered 'ERROR'

过滤配置文件的注释和空行 ​

bash
grep -v -E '^[[:space:]]*(#|$)' config.conf

拆开看:

  • ^[[:space:]]*:行首可以有任意空白
  • (#|$):接下来是注释符号 #,或者直接到行尾(空行)
  • -v:输出不匹配这些情况的行

不要写成 grep -v '^#',它只能去掉顶格的注释,去掉不了一行开头的缩进注释。

在代码中找 TODO ​

bash
grep -rn \
  --include='*.py' \
  --exclude-dir=.git \
  --exclude-dir=venv \
  --exclude-dir=__pycache__ \
  'TODO\|FIXME' .

这里用的是 BRE 的 \|。更推荐写成 ERE:

bash
grep -rnE \
  --include='*.py' \
  --exclude-dir=.git \
  --exclude-dir=venv \
  --exclude-dir=__pycache__ \
  'TODO|FIXME' .

统计每个文件的错误数 ​

bash
grep -rcH 'ERROR' /var/log/*.log | awk -F: '$2 > 0'

grep -rcH 输出 文件名:数量,再用 awk 过滤掉数量为 0 的文件。这是 grep 负责筛选、awk 负责字段处理的典型组合。

提取 IP 地址 ​

bash
grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log | sort | uniq -c | sort -rn

这个正则只保证格式像 IP,不保证每一段都在 0~255 之间。需要严格校验时,用 awk 或专门工具再判断。

提取邮箱地址 ​

bash
grep -Eo '[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}' contacts.txt

邮箱地址的完整语法非常复杂,这个正则适合从日志中做粗略提取,不适合做严格校验。

查找包含某个字符串的文件并批量替换 ​

bash
# 先确认会改哪些文件
grep -rlZF 'old.domain' . | xargs -0 -r echo

# 确认无误后再替换
grep -rlZF 'old.domain' . | xargs -0 -r sed -i 's/old\.domain/new.domain/g'

-Z 让文件名以 NUL 分隔,xargs -0 能正确处理带空格、换行的文件名。批量修改前务必备份,或者先用 sed 输出到临时文件做 diff。替换细节见 sed 命令使用详解。

避免匹配到 grep 自身 ​

bash
ps aux | grep '[n]ginx'

如果写成 grep nginx,grep 自己的命令行里也包含 nginx,通常也会被匹配到。[n]ginx 对 grep 进程来说,命令行里是字面量 [n]ginx,不匹配正则 [n]ginx,因此不会匹配自己。

固定字符串搜索 ​

bash
grep -F 'a.b[c]' file.txt
grep -F -f literals.txt file.txt

搜索代码、配置、URL 中的特殊字符时,-F 可以省去大量转义。

PCRE 环视 ​

bash
# 提取 user= 后面、& 之前的内容
grep -oP '(?<=user=)[^&]+' url.txt

# 匹配后面不是 bar 的 foo
grep -P 'foo(?!bar)' file.txt

环视适合“只匹配位置、不消耗字符”的场景。依赖 GNU grep 的 -P,脚本中要做好兼容性判断。

二进制文件 ​

bash
# 把二进制当文本搜索
grep -a 'TODO' bin.dat

# 忽略二进制文件
grep -I 'TODO' *

空分隔输入 ​

bash
# 输入以 NUL 分隔
printf 'a\0b\0' | grep -z 'b'

# 输出文件名以 NUL 分隔
grep -lZ 'pattern' *.txt | xargs -0 -r echo

-z 适合处理文件名列表、find -print0 输出等以 NUL 分隔的数据;-Z 只影响输出文件名的分隔符。

脚本中的存在性检查 ​

bash
if grep -q '^[[:space:]]*server_name ' /etc/nginx/nginx.conf; then
    echo '已配置 server_name'
fi

grep -q 不需要输出,也不会因为管道或分页器产生副作用,是脚本里最常用的存在性判断方式。

性能建议 ​

  • 优先用 -F:纯字面量搜索比正则快,尤其是大量模式或大文件。
  • 优先用 -E:比 -P 轻量,绝大多数需求 ERE 已经够用。
  • 避免无谓的 .*:grep '.*error.*' 和 grep 'error' 结果一样,后者更快。
  • 能锚定就锚定:^ERROR 比 ERROR 更容易优化。
  • 早停:只关心是否存在时用 -q;只关心前几条时用 -m1;只关心文件名时用 -l。
  • 缩小搜索范围:--include、--exclude-dir、明确的目录,比全盘递归快得多。
  • 不要 cat file | grep:直接 grep pattern file,少一个进程和一次管道拷贝。
  • 设置 LC_ALL=C:处理 ASCII 数据时通常更快,也避免 locale 带来的范围差异。
  • 二进制文件用 -I:避免在二进制内容上做无意义的正则匹配。
  • 超大代码库考虑 rg:ripgrep 默认并行、尊重 .gitignore,在源码搜索场景通常比 grep -r 快很多。
  • 谨慎使用 -P:复杂 PCRE 可能产生灾难性回溯,尤其是嵌套量词和环视。

常见坑 ​

1. BRE 中 + 是普通字符 ​

bash
printf 'aaab\n' | grep 'a+b'    # 不匹配
printf 'aaab\n' | grep -E 'a+b' # 匹配

2. 忘了 -E,| 被当成字面量 ​

bash
grep 'error|warning' app.log     # 找的是包含 error|warning 的行
grep -E 'error|warning' app.log  # 找 error 或 warning

3. Shell 引号与变量展开 ​

bash
# 正确:单引号保护正则
grep '^ERROR' app.log

# 变量需要展开时用双引号
pattern='ERROR'
grep "$pattern" app.log

# 错误:单引号里的 $pattern 不会展开
grep '$pattern' app.log

正则中的 $、反引号、! 都可能被 Shell 解释,含特殊字符时优先用单引号。

4. 模式以 - 开头 ​

bash
grep '-foo' file.txt      # 可能被当成选项
grep -e '-foo' file.txt   # 正确
grep -- '-foo' file.txt   # 也可以

5. grep -c 的退出状态 ​

bash
grep -c 'ERROR' app.log
echo $?   # 没有匹配时输出 0,但退出状态是 1

脚本中要把“计数为 0”和“命令失败”分开处理。

6. 二进制文件只提示文件名 ​

grep 默认不会把二进制文件的匹配内容打印出来,而是输出 Binary file ... matches。需要内容时加 -a,需要忽略时加 -I。

7. -r 与 -R 的符号链接行为 ​

-r 不跟随递归过程中遇到的符号链接,-R 会跟随所有符号链接,后者可能重复搜索甚至陷入循环。大多数场景用 -r。

8. -w 不是“前后必须是空格” ​

-w 的边界是单词字符(字母、数字、下划线)与非单词字符的分界。因此 cat 会匹配 cat.、cat,,但不会匹配 cat_1。

9. [a-z] 受 locale 影响 ​

不同 locale 下字符范围可能不同。处理 ASCII 时用 LC_ALL=C,需要语言相关匹配时用 [[:lower:]] 等字符类。

10. grep -o 不是取字段 ​

grep -o 只输出匹配到的片段,不适合按列取字段。需要按列处理、计算、聚合时用 awk。

11. grep -v 仍然是子串匹配 ​

bash
grep -v 'error' app.log

会排除所有包含 error 的行,包括 error_code、no_error。要精确排除整行,用 grep -vx 'error';排除以 error 开头的行,用 grep -v '^error'。

12. 模式文件中的空行 ​

grep -f patterns.txt 中如果存在空行,空行会被当成空正则,匹配所有行。生成模式文件时要过滤空行:

bash
grep -v '^$' patterns.txt > patterns.clean
grep -f patterns.clean app.log

13. CRLF 模式文件 ​

Windows 换行会让模式末尾带上 \r,导致匹配失败。可以先用 dos2unix 转换,或者用 grep -f <(sed 's/\r$//' patterns.txt)。

14. grep 不支持跨行匹配 ​

grep 按行处理,普通模式无法匹配跨越多行的内容。需要跨行时可以用 pcre2grep -M、rg -U,或者用 sed 的 N/D 命令、awk 的段落模式。

15. 管道颜色与转义序列 ​

bash
grep --color=always 'ERROR' app.log | less -R

--color=always 会把 ANSI 转义序列写进管道。下游不支持颜色时,这些字符会变成乱码;通常用 --color=auto 或 less -R。

16. grep -q 与 pipefail ​

bash
set -o pipefail
yes | grep -q y
echo $?   # 可能是 141,而不是 0

grep -q 提前退出会让上游收到 SIGPIPE。需要稳定判断时,用 grep -q pattern < file,或不要让无限输出的命令进入管道。

17. 递归搜索中的权限错误 ​

bash
grep -r 'pattern' / 2>/dev/null

递归搜索系统目录时会出现大量权限错误。-s 可以抑制这些错误,但也会隐藏真正的文件访问问题;更推荐明确搜索范围。

与其他工具对比 ​

工具擅长不擅长
grep按行筛选、正则搜索、递归查找字段计算、替换、聚合
sed流式替换、删除、插入、按行编辑数组、统计、复杂逻辑
awk字段处理、过滤、统计、报表JSON/XML、复杂 CSV
cut按固定分隔符取列条件判断、正则、计算
sort / uniq排序、相邻去重按字段聚合、保留原顺序去重
rg高速递归代码搜索复杂字段处理、聚合
jqJSON 查询和转换普通文本行处理

典型流水线:

bash
grep 'ERROR' app.log \
  | sed 's/^.*ERROR/ERROR/' \
  | awk '{count[$1]++} END {for (k in count) print count[k], k}'
  • grep:先把关心的行筛出来
  • sed:做轻量的格式清洗
  • awk:做字段提取和统计

选择建议:

  • 只是找包含某内容的行:grep
  • 只是替换、删除、插入文本:sed
  • 需要按列判断、求和、分组、生成报表:awk
  • 需要解析 JSON:jq
  • 需要在大型代码库中快速搜索:ripgrep

速查表 ​

bash
# 基本搜索
grep 'pattern' file.txt

# 忽略大小写
grep -i 'pattern' file.txt

# 反向匹配
grep -v 'pattern' file.txt

# 显示行号
grep -n 'pattern' file.txt

# 只输出匹配部分
grep -o 'pattern' file.txt

# 统计匹配行数
grep -c 'pattern' file.txt

# 统计匹配次数
grep -o 'pattern' file.txt | wc -l

# 整词匹配
grep -w 'word' file.txt

# 整行匹配
grep -x 'whole line' file.txt

# 扩展正则
grep -E 'error|warning' file.txt

# 固定字符串
grep -F 'a.b[c]' file.txt

# 多个模式
grep -e 'foo' -e 'bar' file.txt

# 从文件读取模式
grep -f patterns.txt file.txt

# 递归搜索
grep -rn 'TODO' .

# 只搜索指定后缀
grep -rn --include='*.py' 'TODO' .

# 排除目录
grep -rn --exclude-dir=.git --exclude-dir=node_modules 'TODO' .

# 显示上下文
grep -C2 'ERROR' app.log

# 只列出文件名
grep -rl 'ERROR' /var/log

# 不包含匹配的文件
grep -rL 'ERROR' /var/log

# 前 N 条匹配
grep -m5 'ERROR' app.log

# 静默判断
grep -q 'ERROR' app.log && echo found

# 提取 IP
grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log

# 提取邮箱
grep -Eo '[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}' file.txt

# PCRE 环视
grep -oP '(?<=user=)[^&]+' url.txt

# 文件名 NUL 分隔
grep -rlZ 'pattern' . | xargs -0 -r echo

# 实时过滤
tail -f app.log | grep --line-buffered 'ERROR'

总结 ​

grep 的核心很简单:

  1. 一行一行读
  2. 用正则判断是否匹配
  3. 默认输出整行,或按选项只输出统计/文件名/匹配片段

真正容易出错的是三件事:BRE 与 ERE 的差异、Shell 引号、以及“匹配行”和“匹配次数/字段”的区别。把 -E、-F、-i、-v、-n、-o、-c、-r、--include、-A/-B/-C 这几组选项用熟,日常排障和代码搜索基本就够了。

需要继续加工匹配结果时,交给 sed 做替换,或者交给 awk 做字段统计。

相关文章 ​

参考 ​