用 rg 搜索代码库:先找到现有模式

0阅读9分钟

我踩过最深的坑:没搜就动手

接手一个「给订单加导出功能」的需求,我花了两小时写完导出逻辑,跑测试时发现项目里已经有一个 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 搜索的完整工作流

流程图画布 · 115%
Mermaid 流程图加载中...

这个流程的核心不是记住每一步,而是建立「先搜后写、写完再搜」的肌肉记忆。

工具对比:rg vs grep vs ag vs IDE 搜索

不同工具在不同场景下各有优势。以下是我在实际项目中的对比:

基础能力对比

维度rg (ripgrep)grepag (The Silver Searcher)IDE 搜索
递归搜索默认开启需要 -r 参数默认开启默认开启
.gitignore 感知默认尊重不感知默认尊重部分支持
二进制文件跳过默认跳过不跳过默认跳过默认跳过
文件类型过滤--type ts / -g需要 --include--js手动选目录
多核并行自动单线程多线程取决于实现
JSON 输出--json不支持不支持不支持
正则替换(输出)-r不支持不支持取决于编辑器
Unicode 支持完整依赖 locale部分取决于引擎

性能数据(Linux 内核代码库基准测试)

场景rggit grepGNU grepag
简单模式搜索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 | grepfd 专为文件名搜索设计
搜 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.*\{.*\}'
--jsonJSON 格式输出rg --json 'pattern'
-gglob 过滤文件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' ./src

CEAKSAN 的测评文章里提到,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. 写完代码后再搜一遍自己的改动(确认命名、路径、参数和项目现有模式一致)

常见误区

误区一:「我搜了函数名,没找到,就代表没有」

一个功能可能用不同的名字实现。搜「导出」找不到,试试 downloadexportcsvxlsx。搜「缓存」找不到,试试 memocachestorepersist

业务概念和代码命名之间经常有 gap。多角度搜索不是多余的动作,而是必须的。

误区二:「搜到了就能放心改」

搜到了定义不代表理解了所有调用方。一个函数可能有 30 个调用点,其中 28 个传了标准参数,但有 2 个传了边界值。你需要至少抽查几个调用点,确认你的修改不会破坏它们。

误区三:「rg 够快,搜整个项目没问题」

对于超大型 monorepo(几十万文件),即使 rg 也需要注意搜索范围。可以用 -g 限定目录、--type 限定语言、或者在搜索前先用 fd 确认文件数量。

误区四:「AI 工具会帮我搜」

Claude Code、Cursor、Copilot 确实在内部用了 rg,但它们的搜索策略取决于 prompt 的质量。如果你给 AI 的指令不够明确,它可能只搜了一个关键词就停了。对关键改动,自己跑一遍 rg 确认,比依赖 AI 的搜索行为更可靠。

搜索关键词清单:当你不知道搜什么时

你有的线索搜索关键词方向示例
业务需求描述功能名、实体名rg '订单' -irg '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'
配置项环境变量或配置 keyrg 'ORDER_API_URL'

总结

搜索不是开发的前置步骤,而是开发本身的一部分。每一次 rg 调用都在帮你回答三个问题:

  1. 项目里已经有什么?
  2. 我要改的东西被谁依赖?
  3. 现有的模式告诉我应该怎么做?

ripgrep 让这个过程的摩擦降到几乎为零。它足够快、足够智能、默认行为就对了。但工具再好,关键还是习惯——在动手之前,先搜一下。

参考资料

评论 0

0 / 1000