用 rg 搜索代码库:先找到现有模式
我踩过最深的坑:没搜就动手
接手一个「给订单加导出功能」的需求,我花了两小时写完导出逻辑,跑测试时发现项目里已经有一个 downloadExport() 函数——参数结构、错误处理、文件格式全做好了,我重新造了一个功能一样但命名不同的轮子。
更糟的是,我新加的代码绕过了项目已有的权限校验层。Code review 时同事指出来,我才意识到:这个权限校验在 6 个不同的地方被调用,而我根本没搜过。
这不是个例。BurntSushi 写的 ripgrep 目前在 GitHub 上有 65.5k star,VS Code 的内置搜索、Claude Code 的文件检索、Codex CLI 的代码定位,底层都在用它。一个 2016 年发布的工具,为什么到现在还是整个行业的默认选择?
这篇文章不讲 rg 安装教程。我想聊的是:拿到一个需求之后,怎么用 rg 快速摸清现有代码的模式、边界和调用关系,让修改真正贴合系统本身。
为什么搜索先于编码:理论层面的三个原因
原因一:项目惯例比通用最佳实践更可靠
每个项目都有自己的约定——API 层的封装方式、配置读取的路径、测试目录的组织、组件的命名风格。这些约定通常不会写在文档里,而是藏在代码中。
通用最佳实践告诉你「用 fetch 发请求」,但项目惯例可能是「所有请求必须走 apps/src/api/core,带统一鉴权和错误映射」。如果你不搜直接写,写出来的代码「正确」但不兼容。
原因二:代码的调用关系决定了修改的安全边界
一个函数被 3 个文件调用和被 30 个文件调用,修改策略完全不同。只看定义点,你会以为一个工具函数「很安全,随便改」。但如果你用 rg 搜了调用点,就会发现其中一个调用方传了 null,另一个依赖特定的返回格式。
搜索的本质是还原代码的真实上下文。定义只是接口契约,调用才是真实世界。
原因三:搜索的成本远低于修复的成本
跑一次 rg 需要几十毫秒。修复一个因为不了解现有模式而引入的 bug,可能需要几小时甚至几天。这个成本不对称是「先搜后写」最朴素的理由。
ripgrep 的设计恰好利用了这一点——它足够快,快到搜索几乎不产生心理负担。Andrew Gallant 在 ripgrep 的 benchmark 中展示过:在 Linux 内核代码库上搜索,rg 用 0.082 秒完成的任务,git grep 需要 0.273 秒,GNU grep 需要更久。在一个 13GB 的文件搜索中,rg 用 1.042 秒完成,GNU grep 用了 6.577 秒。
三个真实案例:搜索如何改变我的修改策略
案例一:从路由文件找到权限模式
需求是在后台管理系统新增一个「数据看板」页面。我打算按通用 Next.js 的做法直接建路由文件。
动手前,我跑了一条搜索:
# 找现有的后台路由文件,看它们怎么处理权限
rg -g 'apps/src/app/admin/**/page.tsx' -l结果返回了 12 个页面。我随手打开 3 个,发现它们都在 layout.tsx 里用了一个 <AdminGuard> 组件做权限校验,而不是在每个页面里单独检查。如果我直接新建页面,忘了加这个 guard,就会留下一个无鉴权的后台入口。
搜索帮我省掉的不只是重复代码,而是一个线上安全隐患。
案例二:从错误信息追踪调用链
另一个需求要改一个 formatPrice() 函数的行为——让它支持多币种。我改完签名后跑测试,发现有 4 个测试挂了。
问题不在我的改动本身,而是我没搜过 formatPrice 的调用点:
# 搜所有调用点,不只是函数定义
rg 'formatPrice\(' --type ts -n结果显示这个函数在 23 个文件中被调用,其中有 4 个调用方传的是旧签名(单参数)。如果我先搜了再改,就会选择「保持旧签名兼容 + 新增重载」的方式,而不是直接改签名。
# 更精确地搜:只找传单个参数的调用
rg 'formatPrice\([^,)]+\)' --type ts -n案例三:从样式类名定位组件边界
前端重构时,我需要把一个旧组件的样式迁移到新的设计系统。直接搜组件名只找到 2 个文件,但样式实际上通过全局类名被其他组件引用了。
# 搜样式类名,不只是组件名
rg '\.order-card-' --type css -n
rg 'order-card-header' -n搜出来 8 个文件都在用这个类名,跨了 3 个 feature 模块。如果我只改了组件文件里的样式,另外 7 个地方会全部丢样式。
这个案例的教训是:搜索关键词不只要准,还要多角度。组件名、函数名、类名、错误信息、接口路径,都是入口。
用 rg 搜索的完整工作流
这个流程的核心不是记住每一步,而是建立「先搜后写、写完再搜」的肌肉记忆。
工具对比:rg vs grep vs ag vs IDE 搜索
不同工具在不同场景下各有优势。以下是我在实际项目中的对比:
基础能力对比
| 维度 | rg (ripgrep) | grep | ag (The Silver Searcher) | IDE 搜索 |
|---|---|---|---|---|
| 递归搜索 | 默认开启 | 需要 -r 参数 | 默认开启 | 默认开启 |
| .gitignore 感知 | 默认尊重 | 不感知 | 默认尊重 | 部分支持 |
| 二进制文件跳过 | 默认跳过 | 不跳过 | 默认跳过 | 默认跳过 |
| 文件类型过滤 | --type ts / -g | 需要 --include | --js 等 | 手动选目录 |
| 多核并行 | 自动 | 单线程 | 多线程 | 取决于实现 |
| JSON 输出 | --json | 不支持 | 不支持 | 不支持 |
| 正则替换(输出) | -r | 不支持 | 不支持 | 取决于编辑器 |
| Unicode 支持 | 完整 | 依赖 locale | 部分 | 取决于引擎 |
性能数据(Linux 内核代码库基准测试)
| 场景 | rg | git grep | GNU grep | ag |
|---|---|---|---|---|
| 简单模式搜索 | 0.082s (1.00x) | 0.273s (3.33x) | 0.540s (6.59x) | 0.161s (1.96x) |
| 带行号输出 | 1.66s (1.00x) | - | 9.48s (5.71x) | - |
| 13GB 文件搜索 | 1.042s (1.00x) | - | 6.577s (6.31x) | - |
数据来源:ripgrep 官方 GitHub README 中的 benchmark 及 CEAKSAN 的独立测评。
日常开发场景下的搜索策略对比
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| 找函数/变量引用 | rg 'funcName' -n | 默认跳 node_modules,尊重 gitignore |
| 按语言搜 | rg --type ts 'pattern' | 不需要手写 --include '*.ts' |
| 搜文件路径 | fd 'pattern' 或 rg --files | grep | fd 专为文件名搜索设计 |
| 搜 Git 历史 | git log -S 'pattern' | rg 不覆盖 Git 历史 |
| 搜跨仓库 | gh search code 'pattern' | 本地搜索无法跨仓库 |
| 模糊搜索大项目 | IDE 符号搜索 | 有索引,增量更新 |
rg 关键 Flag 速查
| Flag | 作用 | 典型用法 |
|---|---|---|
-n | 显示行号 | rg -n 'TODO' |
-l | 只列出文件名 | rg -l 'oldAPI' |
-c | 统计匹配数 | rg -c 'FIXME' |
-i | 忽略大小写 | rg -i 'error' |
-w | 全词匹配 | rg -w 'get'(不匹配 getById) |
-F | 固定字符串(不当正则) | rg -F 'user.name' |
-U | 多行搜索 | rg -U 'fn.*\{.*\}' |
--json | JSON 格式输出 | rg --json 'pattern' |
-g | glob 过滤文件 | rg -g '*.tsx' 'Button' |
-t / -T | 包含/排除文件类型 | rg -t js -T test 'import' |
-C 3 | 前后各 3 行上下文 | rg -C 3 'error' |
-r | 替换(只影响输出) | rg 'old' -r 'new' |
--hidden | 搜索隐藏文件 | rg --hidden 'secret' |
--no-ignore | 忽略 gitignore 规则 | rg --no-ignore 'legacy' |
代码对比:从 grep 到 rg 的思维升级
对比一:递归搜索 node_modules 问题
# grep:需要手动排除 node_modules 和 build 目录
grep -r 'useAuth' ./src --exclude-dir=node_modules --exclude-dir=build --exclude-dir=.next
# rg:默认跳过 gitignore 中的目录
rg 'useAuth' ./src用 grep 时,我见过太多人忘记加 --exclude-dir,然后等了几秒发现它在搜 node_modules 里几万个文件。rg 把这个心智负担彻底消除了。
对比二:按语言过滤
# grep:需要用 --include 手动指定扩展名
grep -r 'interface' ./src --include='*.ts' --include='*.tsx'
# rg:内置语言类型识别
rg --type ts 'interface'
# rg 也支持 glob
rg -g '*.{ts,tsx}' 'interface'--type 的优势不只是短——ripgrep 知道 TypeScript 的文件扩展名包括 .ts、.tsx、.mts、.cts,你不需要记住所有扩展名。
对比三:搜索并查看上下文
# grep:需要额外组合 -A 和 -B
grep -rn -A 3 -B 3 'catch' ./src
# rg:-C 一步到位,且输出带颜色高亮
rg -C 3 'catch' ./srcCEAKSAN 的测评文章里提到,rg 的上下文输出带颜色高亮和行号,读起来像「story-like instead of fragmented」——上下文连贯,不是碎片。
对比四:输出为 JSON,接入自动化
# grep:没有 JSON 输出,需要自己用 awk/jq 拼
grep -rn 'TODO' ./src | awk -F: '{print "{\"file\":\""$1"\",\"line\":"$2",\"content\":\""$3"\"}"}'
# rg:原生 JSON 输出
rg --json 'TODO' ./src--json 这个特性正在变得越来越重要。Claude Code、Codex CLI 和越来越多的 AI 编程工具,都依赖 rg 的 JSON 输出来做结构化的代码检索。GrepRAG 的研究论文中也使用了 rg 的 JSON 模式作为 LLM 代码定位的基础设施。
对比五:搜索特定模式,固定字符串 vs 正则
# grep:搜包含点号的字符串需要转义
grep -rn 'user\.name' ./src
# 如果不转义,会匹配 user_name、userName 等
# rg:-F 直接当固定字符串搜
rg -F 'user.name' ./src
# 或者用正则精确匹配
rg 'user\.(name|email)' ./src-F 是一个容易忽略但很实用的 flag。当你搜的内容包含 .、[、( 等正则元字符时,用 -F 比逐个转义省心得多。
对比六:批量查找并验证替换效果
# 先预览替换结果(不会修改文件)
rg 'oldFunction' -r 'newFunction' --type ts
# 确认无误后,配合 xargs 做实际替换
rg 'oldFunction' -l --type ts | xargs sed -i 's/oldFunction/newFunction/g'
# 或者用 rg 的替换 + 写回(ripgrep 本身不修改文件,这是设计哲学)
# 推荐搭配:rg 找 + sed/vim 改ripgrep 的 -r 只影响输出,不修改文件。这是一个有意的设计选择——搜索工具不应该有副作用。需要批量替换时,用 rg -l 找到文件,再交给 sed 或编辑器处理。
搜索前的 12 项检查清单
在动手写代码之前,我通常会过一遍这个清单。不需要每次都做完,但它帮我避开过很多次「先写后搜」的坑:
- 1. 搜项目里是否已有类似功能或工具函数(搜业务关键词,不只是技术名词)
- 2. 搜同名或近义的组件/函数(避免命名冲突或重复实现)
- 3. 搜相关函数的所有调用点(不只是定义点,确认修改影响范围)
- 4. 搜项目已有的错误处理模式(错误怎么包装、怎么上报、怎么展示给用户)
- 5. 搜项目的权限/鉴权实现(新页面或新接口是否需要 guard)
- 6. 搜 API 层封装方式(项目是自己封装了 fetch 还是用 axios/react-query/tanstack)
- 7. 搜样式类名和组件组合方式(不只是组件文件本身)
- 8. 搜配置文件和环境变量(新功能是否需要新的配置项)
- 9. 搜测试目录结构(测试放哪里、怎么命名、mock 怎么组织)
- 10. 搜 Git 历史(
git log -S 'pattern'看这个功能是否曾经存在过但被删了) - 11. 搜文档和 README(项目是否有约定但没写在代码里的规则)
- 12. 写完代码后再搜一遍自己的改动(确认命名、路径、参数和项目现有模式一致)
常见误区
误区一:「我搜了函数名,没找到,就代表没有」
一个功能可能用不同的名字实现。搜「导出」找不到,试试 download、export、csv、xlsx。搜「缓存」找不到,试试 memo、cache、store、persist。
业务概念和代码命名之间经常有 gap。多角度搜索不是多余的动作,而是必须的。
误区二:「搜到了就能放心改」
搜到了定义不代表理解了所有调用方。一个函数可能有 30 个调用点,其中 28 个传了标准参数,但有 2 个传了边界值。你需要至少抽查几个调用点,确认你的修改不会破坏它们。
误区三:「rg 够快,搜整个项目没问题」
对于超大型 monorepo(几十万文件),即使 rg 也需要注意搜索范围。可以用 -g 限定目录、--type 限定语言、或者在搜索前先用 fd 确认文件数量。
误区四:「AI 工具会帮我搜」
Claude Code、Cursor、Copilot 确实在内部用了 rg,但它们的搜索策略取决于 prompt 的质量。如果你给 AI 的指令不够明确,它可能只搜了一个关键词就停了。对关键改动,自己跑一遍 rg 确认,比依赖 AI 的搜索行为更可靠。
搜索关键词清单:当你不知道搜什么时
| 你有的线索 | 搜索关键词方向 | 示例 |
|---|---|---|
| 业务需求描述 | 功能名、实体名 | rg '订单' -i、rg 'order' -i |
| 接口路径 | URL 片段 | rg '/api/v1/orders' |
| 页面 URL | 路由文件路径 | rg -g '**/orders/**' -l |
| 错误提示文案 | 用户可见的错误信息 | rg '库存不足' |
| 数据库字段 | 字段名、模型属性 | rg 'order_status' |
| 样式名 | CSS 类名前缀 | rg '\.order-' --type css |
| 类型定义 | interface/type 名 | rg 'interface Order' |
| 配置项 | 环境变量或配置 key | rg 'ORDER_API_URL' |
总结
搜索不是开发的前置步骤,而是开发本身的一部分。每一次 rg 调用都在帮你回答三个问题:
- 项目里已经有什么?
- 我要改的东西被谁依赖?
- 现有的模式告诉我应该怎么做?
ripgrep 让这个过程的摩擦降到几乎为零。它足够快、足够智能、默认行为就对了。但工具再好,关键还是习惯——在动手之前,先搜一下。
参考资料
- ripgrep 官方 GitHub 仓库 — BurntSushi 维护,包含完整 benchmark 和使用指南
- ripgrep GUIDE.md — 官方入门指南,覆盖
--type、--glob、--json等核心用法 - grep, ripgrep, and AI-Powered Text Search — CEAKSAN — 从 grep 到 rg 的演进分析,包含性能基准数据
- Why ripgrep (rg) Beats grep for Modern Code Search — CodeAnt AI — rg 与 grep 在 2025 年代码搜索场景下的对比
- ripgrep: Not Just a Faster grep, but a Sharper One — DEV Community — 深入 rg 的高级用法:
-F、-U、-r、-o等 - Linux Search Benchmark: Why ripgrep beats grep — Medium — 独立基准测试,包含 Linux 内核和大型代码库数据
- Why Coding Agents Still Use grep as Their Search Backbone — AI 编程工具(Codex CLI 等)为什么选择 rg 作为底层搜索
- GrepRAG: An Empirical Study of Grep-Like Search for LLMs — arXiv — 学术研究,探讨 LLM 如何自主生成 rg 命令做代码定位