09-搜索工具Grep与Glob
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:/' 才是驱动器根。这是跨平台兼容性的硬性细节。
关键设计要点
- UI 复用极致化:GlobTool 直接 re-export GrepTool 的
renderToolResultMessage,userFacingName也统一为'Search',对用户隐藏工具边界。 - 三种 rg 模式自适应:system / embedded / builtin 三种 ripgrep 实现按环境择优,security 注释明确防止 PATH 劫持。
- SIGTERM→SIGKILL 升级:超时杀进程时先 SIGTERM,5 秒后升级到 SIGKILL,应对 ripgrep 在不可中断 I/O 中的"杀不掉"问题。
- EAGAIN 一次性单线程重试:资源受限环境的瞬时错误用
-j 1重试一次,但绝不全局降级,避免大仓库超时。 - 部分结果保留:超时和 buffer overflow 时丢弃可能不完整的最后一行后返回部分结果,让模型至少能利用已得信息。
- ripgrep 隐私化计数:
countFilesRoundedRg把文件数 round 到 10 的幂,跳过 home 目录避免触发 TCC 对话框,体现遥测的隐私意识。
与其他模块的关系
- AgentTool:GrepTool 和 GlobTool 的 prompt 都把"开放式多轮搜索"引导到 AgentTool,形成"精确单次搜索用 Grep/Glob,探索性搜索用 Agent"的分工。
- BashTool:GrepTool prompt 明确禁止模型用 Bash 调 grep/rg,把搜索流量集中到工具系统内。
- permissions/filesystem:
getFileReadIgnorePatterns给 glob 提供 ignore 模式,normalizePatternsToPath把它们规范化到 searchDir 下。 - plugins/orphanedPluginFilter:
getGlobExclusionsForPluginCache排除孤立插件版本目录,避免搜索结果污染。 - components/CtrlOToExpand:非 verbose 模式下搜索结果展示"Ctrl+O 展开"提示,与 FileWriteTool 等共享同一交互范式。
- execFileNoThrow:
testRipgrepOnFirstUse、codesignRipgrepIfNecessary都用这个安全包装调用子进程。
小结
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 在百万行代码库上也能秒级响应模型查询。
更多推荐


所有评论(0)