Dagger TypeScript SDK SearchResult 类完全指南:掌握 grep 搜索结果的结构化读取与源码实现
Dagger TypeScript SDK SearchResult 类完全指南掌握 grep 搜索结果的结构化读取与源码实现【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger导读SearchResult是 Dagger TypeScript SDKdagger.io/dagger客户端中用于承载文本搜索grep结果的核心类。当你对Directory或Workspace调用search方法时引擎会基于 ripgrep 扫描文件内容并把每个命中位置包装成一个SearchResult对象返回。读完本文你将掌握SearchResult的全部字段与方法语义、其底层 GraphQL 类型定义、以及它如何与SearchSubmatch协作实现命中位置精确高亮并能在自己的 Dagger 模块中用类型安全的代码完成跨文件内容检索。SearchResult 在 Dagger API 中的定位SearchResult定义于 TypeScript 客户端代码生成产物 classes/SearchResult.md位于api/client.gen模块即客户端生成代码目录参见 api/client.gen 模块索引。它继承自BaseClient与 SDK 中其他 GraphQL 对象一样构造器仅供内部使用new SearchResult(ctx?, _id?, _absoluteOffset?, _filePath?, _lineNumber?, _matchedLines?)文档明确标注 Constructor is used for internal usage only, do not create object from it.也就是说你不应该自己new SearchResult(...)而是通过Directory.search、Workspace.search等 API 的返回值获取其实例。构造器的可选参数恰好一一对应该对象的持久化字段_absoluteOffsetnumber、_filePathstring、_lineNumbernumber、_matchedLinesstring以及 ID 类型SearchResultID。在 GraphQL schema 层SearchResult被定义为实现Node接口的对象类型见 core/schema/testdata/base_schema.graphqlstype SearchResult implements Node { The byte offset of this line within the file. absoluteOffset: Int! The path to the file that matched. filePath: String! A unique identifier for this SearchResult. id: ID! The first line that matched. lineNumber: Int! The line content that matched. matchedLines: String! Sub-match positions and content within the matched lines. submatches: [SearchSubmatch!]! }每个字段在服务端都是非空!的因此客户端方法返回的Promise解析后不会出现对应字段为null的情况。六个公开方法逐一解析SearchResult类对外暴露 6 个方法全部返回Promise需要在await后取值。下面结合字段语义逐一说明。absoluteOffset()命中的字节偏移async absoluteOffset(): Promisenumber返回该行在文件中的字节偏移量byte offset。注意它定位的是匹配行的起始偏移而非文件中任意字节位置。该值直接来自 ripgrep JSON 输出中的absolute_offset字段见下文源码分析可用于在二进制/大文件中精确跳转到命中位置或与submatches()提供的行内偏移配合计算命中的绝对字节区间。filePath()命中的文件路径async filePath(): Promisestring返回发生匹配的文件路径The path to the file that matched。当一次搜索横跨多个目录、多个文件时用它区分命中来源配合lineNumber()即可拼出经典的路径:行号定位格式。id()对象唯一标识async id(): PromiseSearchResultID返回该 SearchResult 的唯一标识符类型为SearchResultIDSearchResultID string object它本质上是一个带__SearchResultID: never哨兵字段的字符串类型属于 TypeScript 的 branded/nominal typing 技巧——运行时就是一个字符串但编译期不会与普通string或其它 ID 类型混淆。SearchResultID对应 GraphQL 中的scalar SearchResultID见 base_schema.graphqls用于在会话间持久化引用某个具体的搜索结果对象。lineNumber()首个匹配行号async lineNumber(): Promisenumber返回第一个匹配的行号The first line that matched。行号从 1 开始计数对应文件内容中该行的实际行号而不是数组下标。多行模式multiline下一次命中可能跨越多行此字段记录的是起始行。matchedLines()命中的行内容async matchedLines(): Promisestring返回命中的行内容The line content that matched。这是搜索后最常直接消费的字段——拿到命中的整行文本即可用于日志过滤、代码审查、CI 校验等场景。在多行匹配时matchedLines会包含跨行的全部文本以\n连接。submatches()行内子匹配详情async submatches(): PromiseSearchSubmatch[]返回匹配行内的子匹配位置与内容Sub-match positions and content within the matched lines。每个元素是SearchSubmatch对象其方法为start(): Promisenumber—— 匹配在 matchedLines 内的起始偏移end(): Promisenumber—— 匹配在 matchedLines 内的结束偏移text(): Promisestring—— 实际命中的文本片段id(): PromiseSearchSubmatchID—— 子匹配的唯一标识。submatches的价值在于当正则表达式本身含有大量噪音例如整行都被matchedLines返回但你只关心真正命中的单词子匹配能给出精确的命中片段与行内区间便于实现高亮命中词之类的 UI 或精确替换逻辑。GraphQL 侧对应 SearchSubmatch 类型。深入源码SearchResult 的服务端结构与持久化SearchResult并非仅在 SDK 客户端存在它同时是 Dagger 引擎核心对象模型的一部分。核心结构体定义在 core/search.gotype SearchResult struct { FilePath string field:true doc:The path to the file that matched. LineNumber int field:true doc:The first line that matched. AbsoluteOffset int field:true doc:The byte offset of this line within the file. MatchedLines string field:true doc:The line content that matched. Submatches []*SearchSubmatch field:true doc:Sub-match positions and content within the matched lines. }可以看到Go 结构体字段上的doc注释与 GraphQL schema、TypeScript 客户端文档中的描述完全一致——这说明 TypeScript 的SearchResult.md正是由该 schema 代码生成而来三者共享同一套语义。该结构体实现了dagql.PersistedObject与dagql.PersistedObjectDecoder接口core/search.go意味着SearchResult具备跨会话持久化能力EncodePersistedObjectcore/search.go把FilePath、LineNumber、AbsoluteOffset、MatchedLines以及非空的Submatches序列化为 JSON payloadDecodePersistedObjectcore/search.go反向解码从持久化 payload 重建SearchResult对象。这就是SearchResultID存在的意义拿到 ID 后即使搜索上下文已销毁也能在后续调用中恢复引用。SearchSubmatch同样实现了这一对接口core/search.go其持久化字段为Text、Start、End三者。SearchOpts决定 SearchResult 产出方式的参数族要拿到SearchResult需要先调用搜索入口。Directory.search的服务端实现定义在 core/schema/directory.gotype searchArgs struct { core.SearchOpts Paths []string default:[] Globs []string default:[] } func (s *directorySchema) search(ctx context.Context, parent dagql.ObjectResult[*core.Directory], args searchArgs) (dagql.Array[*core.SearchResult], error) { return parent.Self().Search(ctx, parent, args.SearchOpts, true, args.Paths, args.Globs) }搜索选项由SearchOpts定义core/search.go全部参数如下参数类型默认值含义patternstring—必填要匹配的文本正则或字面量字符串literalbooleanfalse将 pattern 视为字面量字符串而非正则表达式multilinebooleanfalse允许跨行搜索dotallbooleanfalse在 multiline 模式下允许.匹配换行符insensitivebooleanfalse启用大小写不敏感匹配skipIgnoredbooleanfalse是否遵循.gitignore、.ignore、.rgignore文件skipHiddenbooleanfalse是否跳过隐藏文件以.开头的文件filesOnlybooleanfalse只返回匹配的文件路径不返回行内容limitnumber不限制限制返回的结果数量在Directory场景下还有两个补充参数paths在指定路径下搜索与globs按 glob 模式过滤文件默认[]。这些选项会逐一映射为底层 ripgrep 命令行参数core/search.goliteral→--fixed-stringsmultiline→--multilinedotall→--multiline-dotallinsensitive→--ignore-case未开启skipIgnored时追加--no-ignore即默认忽略 ignore 文件未开启skipHidden时追加--hidden即默认搜索隐藏文件filesOnly→--files-with-matches否则追加--json始终追加--regexppattern与--no-follow禁止跟随符号链接值得注意的一个实现细节limit没有对应的 ripgrep 标志ripgrep 只能按文件限制结果数无法限制总数因此它是在结果解析阶段生效的见 core/search.go——解析到len(results) *opts.Limit时便停止读取。ripgrep JSON 输出到 SearchResult 的映射过程默认非filesOnly模式下引擎以--json运行 ripgrep并通过parseRgOutputcore/search.go流式解码输出。JSON 的match类型记录结构如下core/search.gotype rgJSON struct { Type string json:type Data struct { Path rgContent json:path Lines rgContent json:lines LineNumber int json:line_number AbsoluteOffset int json:absolute_offset Submatches []struct { Match rgContent json:match Start int json:start End int json:end } json:submatches } json:data }解析时每个字段直接落到SearchResult上core/search.godata.path.text→FilePathdata.line_number→LineNumberdata.absolute_offset→AbsoluteOffsetdata.lines.text→MatchedLinesdata.submatches[*]逐条转为SearchSubmatch{Text, Start, End}同时有两处健壮性处理遇到非 UTF-8 的路径或内容会记录告警并跳过core/search.go保证不会因为个别二进制文件中断整个搜索若 ripgrep 以退出码 1 结束表示无匹配则返回空结果而非报错core/search.go。而在filesOnly模式下则简单地把每行输出作为一个仅含FilePath的SearchResult返回core/search.go此时其余字段为空——这与 GraphQL 类型中字段非空的约束并不冲突因为服务端字段依然按 schema 暴露只是对应值在生成客户端时由引擎保证类型安全。实战在 Dagger 模块中用 SearchResult 检索代码结合以上语义一个典型的 TypeScript 用法是对模块内的源码目录执行正则搜索遍历返回的SearchResult打印文件:行号:内容并对命中词做高亮import { dag, Directory, Container } from dagger.io/dagger; // 假设 src 是一个 Directory 对象例如从 host 目录加载 async function grepSrc(src: Directory): Promisevoid { // 大小写不敏感、忽略 .gitignore 规则、返回行级结果 const results await src.search(todo|fixme, { insensitive: true, }); for (const r of results) { const filePath await r.filePath(); const lineNumber await r.lineNumber(); const offset await r.absoluteOffset(); const lines await r.matchedLines(); const submatches await r.submatches(); console.log(${filePath}:${lineNumber} (byte offset ${offset})); // 用子匹配精确标出命中的文本区间 for (const sm of submatches) { const text await sm.text(); const start await sm.start(); const end await sm.end(); console.log( ^ ${text} [${start}, ${end})); } } }若只想定位文件而不是关心行内容可以开启filesOnly此时每个SearchResult只携带filePath适合做哪些文件包含某模式的清单型任务const files await dir.search(export class, { filesOnly: true }); for (const r of files) { console.log(await r.filePath()); }在 Workspace 场景下引擎会把搜索拆分为本地/底层文件系统与overlay 变更层两部分结果并做按文件的合并参见 core/schema/workspace.go 的mergeSearchResults逻辑并且支持paths、globs等同样的SearchOpts参数core/schema/workspace.go——也就是说无论搜索对象是Directory还是Workspace返回的SearchResult对象结构完全一致上层消费代码可以复用。小结SearchResult是 Dagger 文本搜索能力的统一返回值模型6 个方法覆盖命中定位四要素文件路径、行号、字节偏移、行内容以及精确子匹配submatches与对象 ID服务端核心实现位于 core/search.go由 ripgrep--json输出流式映射而来并支持跨会话持久化GraphQL 契约定义于 base_schema.graphqlsTypeScript 客户端文档与其一一对应搜索入口为Directory.searchcore/schema/directory.go与Workspace.searchcore/schema/workspace.go通过SearchOpts的 9 个选项灵活控制匹配语义。在编写 Dagger 模块时无论你是要做代码规范扫描、CI 中的禁止出现敏感关键字校验还是构建工具链中的源码分析SearchResultSearchSubmatch的组合都能以类型安全的方式把 ripgrep 的原始能力接入你的自动化流水线。【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考