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 极简:
exportconstDESCRIPTION=`- 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 共享:
functionSearchResultSummary({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 三分支
exportfunctionrenderToolResultMessage({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 modeconstfileListContent=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 renderToolResultMessageexportconstrenderToolResultMessage=GrepTool.renderToolResultMessage;直接 re-export GrepTool 的渲染函数——因为 GlobTool 的输出也走files_with_matches形态(一串文件路径),完美匹配 GrepTool 的默认分支。userFacingName()返回'Search'(而不是'Glob'),让 UI 上 Glob 与 Grep 显示一致——对用户而言两者都是"搜索"。
6. utils/ripgrep.ts:三种 rg 模式
getRipgrepConfig()根据运行环境选择三种 ripgrep 实现之一:
constgetRipgrepConfig=memoize(():RipgrepConfig=>{constuserWantsSystemRipgrep=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 hijackingreturn{mode:'system',command:'rg',args:[]}}}if(isInBundledMode()){return{mode:'embedded',command:process.execPath,args:['--no-config'],argv0:'rg'}}constrgRoot=path.resolve(__dirname,'vendor','ripgrep')constcommand=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 升级:
letkillTimeoutId:ReturnType<typeofsetTimeout>|undefinedconsttimeoutId=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 onlyreturn}注释强调"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(newRipgrepTimeoutError(`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()是为交互式搜索优化的流式版本:
exportasyncfunctionripGrepStream(args,target,abortSignal,onLines){// ...letremainder=''child.stdout?.on('data',(chunk:Buffer)=>{constdata=remainder+chunk.toString()constlines=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 的幂:
constmagnitude=Math.floor(Math.log10(count))constpower=Math.pow(10,magnitude)returnMath.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:
constargs=['--files','--glob',searchPattern,'--sort=modified',...(noIgnore?['--no-ignore']:[]),...(hidden?['--hidden']:[]),]// Add ignore patternsfor(constpatternofignorePatterns){args.push('--glob',`!${pattern}`)}// Exclude orphaned plugin version directoriesfor(constexclusionofawaitgetGlobExclusionsForPluginCache(searchDir)){args.push('--glob',exclusion)}constallPaths=awaitripGrep(args,searchDir,abortSignal)注释说明--no-ignore默认 true(不遵守 .gitignore),--hidden默认 true(包含隐藏文件)——这与 BashTool prompt 里"用 Glob 而非 find"的设计意图一致:让模型看到完整文件集,避免被 .gitignore 误隐藏。getGlobExclusionsForPluginCache排除孤立插件版本目录,是 plugin 系统的卫生措施。
13. extractGlobBaseDirectory:绝对路径拆分
ripgrep 的--glob只接受相对模式,所以extractGlobBaseDirectory把绝对路径拆成 baseDir + relativePattern:
exportfunctionextractGlobBaseDirectory(pattern:string){constglobChars=/[*?[{]/constmatch=pattern.match(globChars)if(!match||match.index===undefined){constdir=dirname(pattern)constfile=basename(pattern)return{baseDir:dir,relativePattern:file}}conststaticPrefix=pattern.slice(0,match.index)constlastSepIndex=Math.max(staticPrefix.lastIndexOf('/'),staticPrefix.lastIndexOf(sep))// ...}Windows 下还处理了C:vsC:/的语义差异——'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 在百万行代码库上也能秒级响应模型查询。