在一个前端加上后端的 monorepo 里找一段日志文案,grep -r "timeout" . 跑了四十秒,输出还混着一堆 node_modules 里的压缩文件和二进制乱码。最后是在 IDE 里点开的。
ripgrep(命令名 rg)和 fd 干的就是这件事:在你还没开始优化之前,先把「不该搜的东西」排除掉。它们的快有一半来自这里,不是玄学。
快在什么地方#
先把原因说清楚,才不会对速度有不切实际的期待。
- 默认遵守忽略规则。仓库里的
.gitignore、.ignore、.rgignore会被读取,node_modules、dist、build这类目录自动跳过。grep -r不会读这些。 - 默认跳过二进制和隐藏文件。命中二进制时 grep 会打印一行
Binary file ... matches,rg 直接不搜。 - 并行。多核机器上按文件分片,
fd遍历目录时同样是并行的。 - 正则引擎。Rust 的正则库对字面量做预筛,先用极快的子串匹配定位候选,再做完整匹配。这也是它不支持反向引用和环视的原因(后面会讲)。
所以「快一个数量级」是有条件的:在有明确忽略规则的代码仓库里,差距最大;在全盘或纯文本大目录里,差距会缩到两三倍。具体数字要自己量。
还有一点常被忽略:grep 是单线程的,在大目录上串行地打开、读取、关闭每个文件。文件数越多,系统调用的固定开销越明显,并行的收益也越大。
两者都有各平台的二进制发布,主流包管理器基本都能装。装完用 rg --version 确认,具体安装方式以官方文档为准。有个小坑:部分发行版把 fd 的可执行文件命名成 fdfind,脚本里别硬编码命令名。
常用命令与工具差异#
# 找 TODO,跳过忽略目录,带行号
rg -n --hidden -g '!node_modules' '\bTODO\b' src/
# 只列文件名,按语言类型过滤
rg -l --type py 'deprecated'
# 前后各三行上下文,顺带输出统计
rg -C 3 'panic' --stats
# 用 json 输出接 jq,方便写脚本
rg --json 'unsafe' src/ | jq -r 'select(.type=="match") | .data.path.text'
fd 负责找文件,语法比 find 短得多:
fd -e md -x wc -l # 每个 Markdown 文件的行数
fd -H -t f '^\.env' # 包含隐藏文件,只要普通文件
fd -e ts src/ -x echo {} # 先打印,确认范围再执行
-x(--exec)对每个结果执行命令。它很方便,也很危险,尤其是配合 sed -i 或 rm 时。习惯是先 echo 预览一遍,确认无误再换成真正的命令。
忽略规则比参数更重要#
rg 的效果取决于它跳过了什么。三层规则按优先级:命令行 -g 和 --type 最直接,其次是目录里的 .rgignore、.ignore,最后是 .gitignore。
实际项目里值得做两件事。一是在仓库根放一个 .ignore,把生成目录(dist/、coverage/、vendor/)写进去,这样连没装 git 的环境也能生效。二是用 --type-add 扩展类型定义,把 .vue 之类的后缀归到自定义类型,之后 --type vue 就能用。
反过来,搜不到东西时按顺序检查:是不是被 -g '!...' 排除了、是不是隐藏文件、是不是被判成二进制。rg --debug 会打印它读了哪些忽略文件,比猜快得多。
和 grep / find 的差异#
| 需求 | grep / find | rg / fd |
|---|---|---|
| 读取忽略规则 | 不支持,手动 --exclude |
默认读 .gitignore 等 |
| 二进制文件 | 会打印提示行 | 默认跳过 |
| 多线程 | 无 | 默认启用 |
| 正则语法 | BRE/ERE | Rust regex,无回溯 |
| 反向引用 / 环视 | 部分支持 | 默认不支持,需 -P 切 PCRE2 |
| 内容替换 | 不支持 | 不支持,配 sed 或用 sd |
| 查找文件 | find 表达式 |
fd 模式 + 类型 + 执行 |
| 结构化输出 | 纯文本 | --json |
两个常见误解:rg 不能替换内容(它只负责找),fd 也不是「更快的 find」这么简单——它的默认值更贴近日常使用,比如智能大小写、默认忽略隐藏文件。
自己量一次,别信倍数#
比较必须在公平条件下做,否则量的是「忽略规则」而不是「引擎速度」。
# 公平比较:同一目录,同样的文件范围
time grep -rn --include='*.py' 'deprecated' src/ > /dev/null
time rg -n 'deprecated' --type py src/ > /dev/null
# 让 rg 不忽略任何东西,贴近 grep 的默认行为
time rg -uu -n 'deprecated' src/ > /dev/null
三点要注意:
- 多跑几次。第一次是冷缓存,磁盘还没热,数字会偏高。取三次的中位数。
- 区分两种快。
rg不加参数时快,一部分原因是少搜。-uu之后两者搜的文件集才接近,差距反映的是并行和引擎。 - 看真实目录。拿一个玩具目录测出来的数字没有参考价值,用你天天打开的那个仓库。
顺带一提:在 git 仓库里,git grep 通常也比 grep -r 快,因为它只搜被跟踪的文件。选择顺序可以是 rg → git grep → grep,看你的场景需要哪个。
边界与日常习惯#
- 需要 PCRE 特性。反向引用、环视这些 Rust regex 不支持,加
-P切到 PCRE2 引擎,或者回退到grep -P。 - 脚本要可移植。CI 镜像里不一定装了 rg。写给别人用的 shell 脚本,
grep更保险;自己日常用,rg 更舒服。 - 要跟随符号链接。rg 和 fd 默认不跟随,需要显式
-L,否则会漏文件而且毫无提示。 - 编码问题。Windows 上的 UTF-16 文件、GBK 文本,rg 可能直接当二进制跳过。加
-a强制按文本处理,但要接受乱码风险。 - 超大单文件或流。
tail -f app.log | rg pattern是常见用法,但别对几个 G 的文件做全量正则回溯。 - 批量修改。
fd -x配合sed -i出错时没有撤销。生产目录里先备份,或者用git status兜底。
还有一个容易踩的:忽略规则的优先级。.rgignore 会覆盖 .gitignore,--no-ignore 又全部关掉。搜索「明明存在却搜不到」时,先用 rg --files -uu | head 看看文件到底在不在结果集里。
把这些固化成习惯#
工具的价值在于变成肌肉记忆。几个值得背下来的组合:
rg -l pattern | xargs -r 命令:找到文件再批量处理,比-x更好控制。fd -e py -x ruff check {}:只对某类文件跑检查,范围可控,也方便配合--changed-within限定最近改动的文件。rg --files | wc -l:快速知道仓库有多少文件,判断要不要加忽略规则。rg -c pattern:按文件统计命中行数,接一个sort就能看出热点在哪个目录。
结论#
- rg 和 fd 的快,一半来自默认忽略,一半来自并行和引擎,先理解这一点再用。
- 日常搜索直接上
rg,找文件用fd,别背find的表达式。 - 比速度要在同一目录、同一忽略策略下量,并且区分「少搜」和「搜得快」。
- 需要反向引用、可移植脚本、跟随软链接时,老老实实回退到 grep / find。
fd -x前先echo预览,这条能省掉一次误删。
完