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 SDK(@dagger.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 的返回值获取其实例。构造器的可选参数恰好一一对应该对象的持久化字段:_absoluteOffset(number)、_filePath(string)、_lineNumber(number)、_matchedLines(string)以及 ID 类型SearchResultID。
在 GraphQL schema 层,SearchResult被定义为实现Node接口的对象类型,见 core/schema/testdata/base_schema.graphqls:
type 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(): Promise<number>返回"该行在文件中的字节偏移量"(byte offset)。注意它定位的是匹配行的起始偏移,而非文件中任意字节位置。该值直接来自 ripgrep JSON 输出中的absolute_offset字段(见下文源码分析),可用于在二进制/大文件中精确跳转到命中位置,或与submatches()提供的行内偏移配合计算命中的绝对字节区间。
filePath():命中的文件路径
async filePath(): Promise<string>返回发生匹配的文件路径(The path to the file that matched)。当一次搜索横跨多个目录、多个文件时,用它区分命中来源;配合lineNumber()即可拼出经典的路径:行号定位格式。
id():对象唯一标识
async id(): Promise<SearchResultID>返回该 SearchResult 的唯一标识符,类型为SearchResultID:
SearchResultID = string & object
它本质上是一个带__SearchResultID: never哨兵字段的字符串类型,属于 TypeScript 的 branded/nominal typing 技巧——运行时就是一个字符串,但编译期不会与普通string或其它 ID 类型混淆。SearchResultID对应 GraphQL 中的scalar SearchResultID(见 base_schema.graphqls),用于在会话间持久化引用某个具体的搜索结果对象。
lineNumber():首个匹配行号
async lineNumber(): Promise<number>返回"第一个匹配的行号"(The first line that matched)。行号从 1 开始计数,对应文件内容中该行的实际行号,而不是数组下标。多行模式(multiline)下,一次命中可能跨越多行,此字段记录的是起始行。
matchedLines():命中的行内容
async matchedLines(): Promise<string>返回"命中的行内容"(The line content that matched)。这是搜索后最常直接消费的字段——拿到命中的整行文本,即可用于日志过滤、代码审查、CI 校验等场景。在多行匹配时,matchedLines会包含跨行的全部文本(以\n连接)。
submatches():行内子匹配详情
async submatches(): Promise<SearchSubmatch[]>返回"匹配行内的子匹配位置与内容"(Sub-match positions and content within the matched lines)。每个元素是SearchSubmatch对象,其方法为:
start(): Promise<number>—— 匹配在 matchedLines 内的起始偏移;end(): Promise<number>—— 匹配在 matchedLines 内的结束偏移;text(): Promise<string>—— 实际命中的文本片段;id(): Promise<SearchSubmatchID>—— 子匹配的唯一标识。
submatches的价值在于:当正则表达式本身含有大量"噪音"(例如整行都被matchedLines返回,但你只关心真正命中的单词),子匹配能给出精确的命中片段与行内区间,便于实现"高亮命中词"之类的 UI 或精确替换逻辑。GraphQL 侧对应 SearchSubmatch 类型。
深入源码:SearchResult 的服务端结构与持久化
SearchResult并非仅在 SDK 客户端存在,它同时是 Dagger 引擎核心对象模型的一部分。核心结构体定义在 core/search.go:
type 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具备跨会话持久化能力:
EncodePersistedObject(core/search.go)把FilePath、LineNumber、AbsoluteOffset、MatchedLines以及非空的Submatches序列化为 JSON payload;DecodePersistedObject(core/search.go)反向解码,从持久化 payload 重建SearchResult对象。
这就是SearchResultID存在的意义:拿到 ID 后,即使搜索上下文已销毁,也能在后续调用中恢复引用。SearchSubmatch同样实现了这一对接口(core/search.go),其持久化字段为Text、Start、End三者。
SearchOpts:决定 SearchResult 产出方式的参数族
要拿到SearchResult,需要先调用搜索入口。Directory.search的服务端实现定义在 core/schema/directory.go:
type 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),全部参数如下:
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
pattern | string | —(必填) | 要匹配的文本(正则或字面量字符串) |
literal | boolean | false | 将 pattern 视为字面量字符串而非正则表达式 |
multiline | boolean | false | 允许跨行搜索 |
dotall | boolean | false | 在 multiline 模式下允许.匹配换行符 |
insensitive | boolean | false | 启用大小写不敏感匹配 |
skipIgnored | boolean | false | 是否遵循.gitignore、.ignore、.rgignore文件 |
skipHidden | boolean | false | 是否跳过隐藏文件(以.开头的文件) |
filesOnly | boolean | false | 只返回匹配的文件路径,不返回行内容 |
limit | number | 不限制 | 限制返回的结果数量 |
在Directory场景下还有两个补充参数paths(在指定路径下搜索)与globs(按 glob 模式过滤文件,默认[])。
这些选项会逐一映射为底层 ripgrep 命令行参数(core/search.go):
literal→--fixed-stringsmultiline→--multilinedotall→--multiline-dotallinsensitive→--ignore-case- 未开启
skipIgnored时追加--no-ignore(即默认忽略 ignore 文件) - 未开启
skipHidden时追加--hidden(即默认搜索隐藏文件) filesOnly→--files-with-matches,否则追加--json- 始终追加
--regexp=<pattern>与--no-follow(禁止跟随符号链接)
值得注意的一个实现细节:limit没有对应的 ripgrep 标志(ripgrep 只能按文件限制结果数,无法限制总数),因此它是在结果解析阶段生效的(见 core/search.go)——解析到len(results) >= *opts.Limit时便停止读取。
ripgrep JSON 输出到 SearchResult 的映射过程
默认(非filesOnly)模式下,引擎以--json运行 ripgrep,并通过parseRgOutput(core/search.go)流式解码输出。JSON 的match类型记录结构如下(core/search.go):
type 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.go):
data.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): Promise<void> { // 大小写不敏感、忽略 .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.graphqls,TypeScript 客户端文档与其一一对应;
- 搜索入口为
Directory.search(core/schema/directory.go)与Workspace.search(core/schema/workspace.go),通过SearchOpts的 9 个选项灵活控制匹配语义。
在编写 Dagger 模块时,无论你是要做代码规范扫描、CI 中的"禁止出现敏感关键字"校验,还是构建工具链中的源码分析,SearchResult+SearchSubmatch的组合都能以类型安全的方式把 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),仅供参考