9. 搜索工具 Grep 与 Glob

所属分组:工具系统

概述

GrepTool 与 GlobTool 是 Claude Code 的两个搜索工具——前者基于 ripgrep 做内容搜索,后者基于 rg --files --glob 做文件名匹配。两者在 prompt 设计上各自侧重:GrepTool 详细说明 ripgrep 语法与三种 output mode,GlobTool 则强调"按修改时间排序"与"开放式搜索用 AgentTool"。在 UI 层面两者高度复用——GlobTool 直接 re-export GrepTool 的 renderToolResultMessage,因为它们共享 SearchResultSummary 组件。

底层 utils/ripgrep.ts 是一个完整的 ripgrep 进程管理器:它处理 system/builtin/embedded 三种 rg 模式、EAGAIN 重试、超时 SIGKILL 升级、流式输出等。utils/glob.ts 则把 rg --files --glob 包装成简洁的 glob() 函数。本文从 prompt、UI、底层 utils 三个层面剖析这对搜索双胞胎。

源码位置

  • [tools/GrepTool/prompt.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/GrepTool/prompt.ts)
  • [tools/GrepTool/UI.tsx](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/GrepTool/UI.tsx)
  • [tools/GlobTool/prompt.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/GlobTool/prompt.ts)
  • [tools/GlobTool/UI.tsx](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/GlobTool/UI.tsx)
  • [utils/ripgrep.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/utils/ripgrep.ts)
  • [utils/glob.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/utils/glob.ts)

核心实现分析

1. GrepTool prompt:明确"NEVER invoke grep or rg as a Bash command"

getDescription() 短小但语义密集:

return `A powerful search tool built on ripgrep

  Usage:
  - ALWAYS use ${GREP_TOOL_NAME} for search tasks. NEVER invoke \`grep\` or \`rg\` as a ${BASH_TOOL_NAME} command. The ${GREP_TOOL_NAME} tool has been optimized for correct permissions and access.
  - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
  - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
  - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
  - Use ${AGENT_TOOL_NAME} tool for open-ended searches requiring multiple rounds
  - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
  - Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, use \`multiline: true\`
`

几个值得关注的点:① 直接禁止模型用 Bash 调用 grep/rg,理由是"optimized for correct permissions and access"——权限和访问路径已在工具内做了正确处理;② 三种 output mode 明确列出(content / files_with_matches / count),让模型按需选择;③ 专门提示 Go 代码中 interface{} 的转义陷阱(ripgrep 字面括号需转义);④ 跨行匹配需显式 multiline: true,避免默认贪婪匹配误伤。

2. GlobTool prompt:四条短描述

GlobTool 的 prompt 极简:

export const DESCRIPTION = `- Fast file pattern matching tool that works with any codebase size
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
- Returns matching file paths sorted by modification time
- Use this tool when you need to find files by name patterns
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead`

第三条"sorted by modification time"是关键特性——--sort=modified 让最近修改的文件排在前,这对模型定位"最近改过的文件"非常有用。第五条把"开放式多轮搜索"引导到 AgentTool,与 GrepTool 形成职责切分。

3. SearchResultSummary:搜索结果的统一渲染

GrepTool UI 中定义的 SearchResultSummary 组件被三种 output mode 共享:

function SearchResultSummary({ count, countLabel, secondaryCount, secondaryLabel, content, verbose }) {
  // primary: "Found 3 files"
  // secondary: "across 5 directories"
  // verbose: 显示 ⎿ 缩进的 content
  // non-verbose: 单行 + CtrlOToExpand 提示
}

它的精妙之处在于"复数处理"——当 count 为 1 时把 countLabel.slice(0, -1) 去掉尾部 ‘s’,让显示从"1 files"变成"1 file"。secondaryCount / secondaryLabel 用于 count 模式(Found 5 matches across 3 files)。verbose 模式额外渲染 缩进的 content,非 verbose 模式仅显示一行带 CtrlOToExpand 提示。

4. GrepTool 的 renderToolResultMessage 三分支

export function renderToolResultMessage({ mode = 'files_with_matches', filenames, numFiles, content, numLines, numMatches }, _progress, { verbose }) {
  if (mode === 'content') {
    return <SearchResultSummary count={numLines ?? 0} countLabel="lines" content={content} verbose={verbose} />
  }
  if (mode === 'count') {
    return <SearchResultSummary count={numMatches ?? 0} countLabel="matches" secondaryCount={numFiles} secondaryLabel="files" content={content} verbose={verbose} />
  }
  // files_with_matches mode
  const fileListContent = filenames.map(filename => filename).join('\n')
  return <SearchResultSummary count={numFiles} countLabel="files" content={fileListContent} verbose={verbose} />
}

content 模式报 lines 数,count 模式报 matches+files 双重计数,files_with_matches 模式报 files 数——三种模式各自选择最有信息量的统计维度。

5. GlobTool UI:完全复用 GrepTool 渲染

GlobTool 的 UI 文件极其精简,最有意思的是这一行:

// Note: GlobTool reuses GrepTool's renderToolResultMessage
export const renderToolResultMessage = GrepTool.renderToolResultMessage;

直接 re-export GrepTool 的渲染函数——因为 GlobTool 的输出也走 files_with_matches 形态(一串文件路径),完美匹配 GrepTool 的默认分支。userFacingName() 返回 'Search'(而不是 'Glob'),让 UI 上 Glob 与 Grep 显示一致——对用户而言两者都是"搜索"。

6. utils/ripgrep.ts:三种 rg 模式

getRipgrepConfig() 根据运行环境选择三种 ripgrep 实现之一:

const getRipgrepConfig = memoize((): RipgrepConfig => {
  const userWantsSystemRipgrep = isEnvDefinedFalsy(process.env.USE_BUILTIN_RIPGREP)
  if (userWantsSystemRipgrep) {
    const { cmd: systemPath } = findExecutable('rg', [])
    if (systemPath !== 'rg') {
      // SECURITY: Use command name 'rg' instead of systemPath to prevent PATH hijacking
      return { mode: 'system', command: 'rg', args: [] }
    }
  }
  if (isInBundledMode()) {
    return { mode: 'embedded', command: process.execPath, args: ['--no-config'], argv0: 'rg' }
  }
  const rgRoot = path.resolve(__dirname, 'vendor', 'ripgrep')
  const command = process.platform === 'win32'
    ? path.resolve(rgRoot, `${process.arch}-win32`, 'rg.exe`)
    : path.resolve(rgRoot, `${process.arch}-${process.platform}`, 'rg')
  return { mode: 'builtin', command, args: [] }
})

注释里有一处安全细节:找到系统 rg 后仍用 'rg' 字面名而非完整路径,防止"恶意 ./rg.exe 在当前目录"被 PATH 解析命中——OS 的 NoDefaultCurrentDirectoryInExePath 保护会让 'rg' 安全解析。三种模式分别是:

  • system:用户系统装的 rg(USE_BUILTIN_RIPGREP=false 时启用)
  • embedded:bun 打包模式下,rg 静态编译进 bun,通过 argv0='rg' 派生
  • builtin:vendored 的预编译 rg 二进制(按 arch-platform 分目录存放)

7. ripGrepRaw 的 SIGTERM→SIGKILL 升级

embedded 模式下用 spawn 而非 execFile(因为 execFile 不支持 argv0)。超时处理做了 SIGTERM→SIGKILL 升级:

let killTimeoutId: ReturnType<typeof setTimeout> | undefined
const timeoutId = setTimeout(() => {
  if (process.platform === 'win32') {
    child.kill()
  } else {
    child.kill('SIGTERM')
    killTimeoutId = setTimeout(c => c.kill('SIGKILL'), 5_000, child)
  }
}, timeout)

注释解释:ripgrep 在不可中断 I/O(深度文件系统遍历)状态下,SIGTERM 可能被忽略。5 秒后升级到 SIGKILL 才能保证进程结束。Windows 上 child.kill('SIGTERM') 会抛错,所以用默认 signal。

8. EAGAIN 重试的单线程降级

ripGrep() 在处理 EAGAIN 错误时做了一次性单线程重试:

if (!isRetry && isEagainError(stderr)) {
  logForDebugging(`rg EAGAIN error detected, retrying with single-threaded mode (-j 1)`)
  logEvent('tengu_ripgrep_eagain_retry', {})
  ripGrepRaw(args, target, abortSignal, (retryError, retryStdout, retryStderr) => {
    handleResult(retryError, retryStdout, retryStderr, true)
  }, true) // Force single-threaded mode for this retry only
  return
}

注释强调"Persisting single-threaded mode globally caused timeouts on large repos where EAGAIN was just a transient startup error"——EAGAIN 是 Docker/CI 资源受限环境的瞬时启动错误,全局降级到单线程会让大仓库超时。所以只对当次调用降级。

9. RipgrepTimeoutError 与部分结果保留

超时时不直接 reject,而是判断是否有部分输出:

if (isTimeout && lines.length === 0) {
  reject(new RipgrepTimeoutError(
    `Ripgrep search timed out after ${getPlatform() === 'wsl' ? 60 : 20} seconds. The search may have matched files but did not complete in time. Try searching a more specific path or pattern.`,
    lines,
  ))
  return
}
resolve(lines)

有部分输出时丢弃最后一行(可能不完整)后返回——让模型至少能看到部分匹配,而不是完全失败。WSL 默认 60 秒,其他平台 20 秒,反映了 WSL2 文件读取 3-5x 性能惩罚。

10. ripGrepStream 的流式输出

ripGrepStream() 是为交互式搜索优化的流式版本:

export async function ripGrepStream(args, target, abortSignal, onLines) {
  // ...
  let remainder = ''
  child.stdout?.on('data', (chunk: Buffer) => {
    const data = remainder + chunk.toString()
    const lines = data.split('\n')
    remainder = lines.pop() ?? ''
    if (lines.length) onLines(lines.map(stripCR))
  })
  // ...
}

注释提到这是 fzf 的 change:reload 模式——“first results paint while rg is still walking the tree”。remainder 跨 chunk 保留半行,避免行被切断。AbortSignal 用于提前停止(例如用户输入变化触发新搜索)。

11. countFilesRoundedRg 的隐私化文件计数

countFilesRoundedRg() 用于遥测,把文件数 round 到最近的 10 的幂:

const magnitude = Math.floor(Math.log10(count))
const power = Math.pow(10, magnitude)
return Math.round(count / power) * power

注释示例:“8 → 10, 42 → 100, 350 → 100, 750 → 1000”。这是隐私保护——避免把仓库确切文件数上报。函数还跳过 home 目录计数,避免触发 macOS TCC 权限对话框(Desktop/Downloads/Documents 等需要用户授权)。

12. utils/glob.ts:把 rg --files 包装成 glob()

glob() 函数把 ripgrep 的 --files 模式包装成简洁的文件名匹配 API:

const args = [
  '--files',
  '--glob', searchPattern,
  '--sort=modified',
  ...(noIgnore ? ['--no-ignore'] : []),
  ...(hidden ? ['--hidden'] : []),
]
// Add ignore patterns
for (const pattern of ignorePatterns) {
  args.push('--glob', `!${pattern}`)
}
// Exclude orphaned plugin version directories
for (const exclusion of await getGlobExclusionsForPluginCache(searchDir)) {
  args.push('--glob', exclusion)
}
const allPaths = await ripGrep(args, searchDir, abortSignal)

注释说明 --no-ignore 默认 true(不遵守 .gitignore),--hidden 默认 true(包含隐藏文件)——这与 BashTool prompt 里"用 Glob 而非 find"的设计意图一致:让模型看到完整文件集,避免被 .gitignore 误隐藏。getGlobExclusionsForPluginCache 排除孤立插件版本目录,是 plugin 系统的卫生措施。

13. extractGlobBaseDirectory:绝对路径拆分

ripgrep 的 --glob 只接受相对模式,所以 extractGlobBaseDirectory 把绝对路径拆成 baseDir + relativePattern:

export function extractGlobBaseDirectory(pattern: string) {
  const globChars = /[*?[{]/
  const match = pattern.match(globChars)
  if (!match || match.index === undefined) {
    const dir = dirname(pattern)
    const file = basename(pattern)
    return { baseDir: dir, relativePattern: file }
  }
  const staticPrefix = pattern.slice(0, match.index)
  const lastSepIndex = Math.max(staticPrefix.lastIndexOf('/'), staticPrefix.lastIndexOf(sep))
  // ...
}

Windows 下还处理了 C: vs C:/ 的语义差异——'C:' 是"驱动器 C 的当前目录"(相对路径),'C:/' 才是驱动器根。这是跨平台兼容性的硬性细节。

关键设计要点

  1. UI 复用极致化:GlobTool 直接 re-export GrepTool 的 renderToolResultMessageuserFacingName 也统一为 'Search',对用户隐藏工具边界。
  2. 三种 rg 模式自适应:system / embedded / builtin 三种 ripgrep 实现按环境择优,security 注释明确防止 PATH 劫持。
  3. SIGTERM→SIGKILL 升级:超时杀进程时先 SIGTERM,5 秒后升级到 SIGKILL,应对 ripgrep 在不可中断 I/O 中的"杀不掉"问题。
  4. EAGAIN 一次性单线程重试:资源受限环境的瞬时错误用 -j 1 重试一次,但绝不全局降级,避免大仓库超时。
  5. 部分结果保留:超时和 buffer overflow 时丢弃可能不完整的最后一行后返回部分结果,让模型至少能利用已得信息。
  6. ripgrep 隐私化计数countFilesRoundedRg 把文件数 round 到 10 的幂,跳过 home 目录避免触发 TCC 对话框,体现遥测的隐私意识。

与其他模块的关系

  • AgentTool:GrepTool 和 GlobTool 的 prompt 都把"开放式多轮搜索"引导到 AgentTool,形成"精确单次搜索用 Grep/Glob,探索性搜索用 Agent"的分工。
  • BashTool:GrepTool prompt 明确禁止模型用 Bash 调 grep/rg,把搜索流量集中到工具系统内。
  • permissions/filesystemgetFileReadIgnorePatterns 给 glob 提供 ignore 模式,normalizePatternsToPath 把它们规范化到 searchDir 下。
  • plugins/orphanedPluginFiltergetGlobExclusionsForPluginCache 排除孤立插件版本目录,避免搜索结果污染。
  • components/CtrlOToExpand:非 verbose 模式下搜索结果展示"Ctrl+O 展开"提示,与 FileWriteTool 等共享同一交互范式。
  • execFileNoThrowtestRipgrepOnFirstUsecodesignRipgrepIfNecessary 都用这个安全包装调用子进程。

小结

GrepTool 与 GlobTool 是一对"职责互补 + UI 共享"的搜索双胞胎。GrepTool 用详细 prompt 教模型 ripgrep 语法和三种 output mode,GlobTool 用极简 prompt 强调"按修改时间排序"与"开放式搜索转 AgentTool"。底层 utils/ripgrep.ts 是一个完整的 rg 进程管理器,处理三种实现模式、EAGAIN 重试、SIGTERM→SIGKILL 升级、流式输出、隐私化计数等真实工程问题。utils/glob.ts 则把 rg --files --glob 包装成简洁 API,处理绝对路径拆分与跨平台 Windows 驱动器语义。这套搜索基础设施让 Claude Code 在百万行代码库上也能秒级响应模型查询。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐