yq 的 with 操作符实战指南:深层路径批量更新与数组元素相对更新
2026/9/14 10:58:36 网站建设 项目流程

yq 的 with 操作符实战指南:深层路径批量更新与数组元素相对更新

【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq

with是 yq 表达式语言中一个非常实用的复合操作符,它让你可以把"定位到目标路径"与"在该路径上执行的更新操作"分离:第一个参数表达式负责设置新的上下文根节点(root context),第二个表达式则以该根节点为基准运行。本文以仓库文档 pkg/yqlib/doc/operators/with.md 为主体,结合 operator_with.go 源码与 operator_with_test.go 测试用例,系统讲解with的语法、执行原理与三个高频实战场景,帮助你写出更简洁、可维护的 YAML/JSON 更新表达式。

with 操作符的核心语义

根据 with.md 的官方定义:

Use thewithoperator to conveniently make multiple updates to a deeply nested path, or to update array elements relatively to each other. The first argument expression sets the root context, and the second expression runs against that root context.

with操作符用于解决两个典型问题:

  1. 对深层嵌套路径做多处更新:无需反复书写冗长的路径前缀;
  2. 让数组元素之间进行相对更新:第二个表达式会以数组的每一个元素作为上下文根节点运行,因此可以在表达式内部用.引用元素自身,实现"相对于元素"的更新。

其通用语法为:

yq 'with(<路径表达式>; <更新表达式块>)' 文件

注意两个参数之间使用分号;连接,这在 yq 中表示一个"块(block)"。第一个参数是路径定位表达式,第二个参数可以是任意由|;串联起来的更新表达式。

从源码看 with 的执行流程

语法注册与优先级

with在词法层面由 lexer_participle.go 注册为普通操作符(simpleOp("with", withOpType)),其操作类型定义在 operation.go:

var withOpType = &operationType{Type: "WITH", NumArgs: 1, Precedence: 52, Handler: withOperator, CheckForPostTraverse: true}
  • Precedence: 52:与mapfilteromiteval等操作符同级,属于高优先级表达式;
  • CheckForPostTraverse: true:允许在with(...)之后继续追加后置遍历操作(例如with(...)[]这种 splat 写法,见下文测试用例);
  • NumArgs: 1with本身只接收一个参数,第二个表达式通过分号块(block)传入,这一点在下面的实现中会看到。

分号块;本身由 lexer_participle.go 映射为blockOpType,定义于 operation.go:

var blockOpType = &operationType{Type: "BLOCK", Precedence: 10, NumArgs: 2, Handler: emptyOperator}

也就是说,with(A; B)在解析后,with的右子树(RHS)是一个 BLOCK 节点,其 LHS 是路径表达式 A,RHS 是更新表达式 B。

withOperator 的实现原理

核心实现位于 operator_with.go:

func withOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { // with(path, exp) if expressionNode.RHS.Operation.OperationType != blockOpType { return Context{}, fmt.Errorf("with must be given a block (;), got %v instead", expressionNode.RHS.Operation.OperationType.Type) } pathExp := expressionNode.RHS.LHS updateContext, err := d.GetMatchingNodes(context, pathExp) if err != nil { return Context{}, err } updateExp := expressionNode.RHS.RHS for el := updateContext.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) _, err = d.GetMatchingNodes(updateContext.SingleChildContext(candidate), updateExp) if err != nil { return Context{}, err } } return context, nil }

整个执行流程可以拆解为四步:

  1. 校验块结构with的第二个参数必须是分号块(;),否则直接报错with must be given a block (;)。这保证了第一个参数负责"定位"、第二个参数负责"更新"的语法约束;
  2. 定位目标节点:对第一个路径表达式调用GetMatchingNodes(定义见 data_tree_navigator.go),得到所有匹配的候选节点集合updateContext
  3. 逐个执行更新:遍历updateContext.MatchingNodes中的每一个候选节点,通过SingleChildContext(candidate)(定义见 context.go)把该节点包装成一个只包含单个节点的新上下文,然后以它为根节点执行第二个更新表达式;
  4. 返回原上下文:最终返回进入时的context,因此整个with表达式的输出仍是完整文档,更新通过节点引用原地生效。

正是第 3 步的"逐个节点、单节点上下文"设计,决定了with对数组的天然友好性:如果第一个参数匹配到多个节点(例如.myArray[]),第二个表达式会被反复执行,每次都以当前元素为.,从而实现"元素间相对更新"。

场景一:更新深层节点并同时修改样式

当需要修改一个深层节点的值、并且希望一并调整它的输出样式(如强制单引号)时,with可以避免写两次完整路径。

给定sample.yml

a: deeply: nested: value

执行:

yq 'with(.a.deeply.nested; . = "newValue" | . style="single")' sample.yml

输出:

a: deeply: nested: 'newValue'

这里第二个表达式块中发生了两件事:

  • . = "newValue":把上下文根节点(即.a.deeply.nested)的值赋值为newValue
  • . style="single":对该节点设置 YAML 样式属性为单引号。yq 中的样式赋值(styletaganchorcomment等)由assignStyleOperator/assignAttributesOperator一族操作符处理(见 operation.go),样式设置细节可参考 style.md。

注意.在这里代表"上下文根节点本身",而不是"当前文档根",这正是with重新设置 root context 的直观体现。

场景二:批量更新多个深层嵌套属性

如果目标是一个深层 map,想要同时更新它的多个子属性,with可以把路径前缀只写一次。

给定sample.yml

a: deeply: nested: value other: thing

执行:

yq 'with(.a.deeply; .nested = "newValue" | .other= "newThing")' sample.yml

输出:

a: deeply: nested: newValue other: newThing

对比不使用with的写法:

yq '.a.deeply.nested = "newValue" | .a.deeply.other = "newThing"' sample.yml

显然,with将重复出现的.a.deeply前缀收敛为一次,后续更新全部相对该根节点进行,表达式更短、更易读,后续维护时也只需改一处路径。这也印证了 with.md 中对"conveniently make multiple updates to a deeply nested path"的描述。

场景三:相对更新数组元素

这是with最具特色的能力:第二个表达式会以数组的每一个元素作为上下文根节点运行,因此可以写出"相对于元素自身"的更新逻辑。

给定sample.yml

myArray: - a: apple - a: banana

执行:

yq 'with(.myArray[]; .b = .a + " yum")' sample.yml

输出:

myArray: - a: apple b: apple yum - a: banana b: banana yum

with的循环逐个处理数组元素(对应源码第 23-30 行的遍历逻辑):对第一个元素,.{a: apple},于是生成b: "apple yum";对第二个元素同理生成b: "banana yum"。两个元素的更新互不干扰,且都写回了原数组。

进阶:结合复合赋值运算符

在 operator_with_test.go 中还有一个未写入文档的测试场景,演示了+=复合赋值的相对更新:

yq 'with(.myArray[]; .a += .a)' sample.yml

输出:

myArray: - a: appleapple - a: bananabanana

这里每个元素都把自己的a值翻倍拼接,进一步说明在with的第二个表达式中,.a是相对于当前元素解析的,而非相对于文档根。类似的*=-=等复合赋值操作符(见 operation.go)都可以与with组合使用。

语法约束与常见错误

必须使用分号块

with的第二个参数必须是以;构成的分块表达式,否则运行时会抛出错误:

with must be given a block (;), got XXX instead

这正是源码中expressionNode.RHS.Operation.OperationType != blockOpType校验的结果。因此with(.a; .b = 1)合法,而把第二个表达式直接作为逗号参数或独立表达式都会失败。

后置遍历(splat)

由于withOpType声明了CheckForPostTraverse: truewith(...)之后可以继续追加[]等后置遍历操作。operator_with_test.go 中的 "with splat" 场景验证了这一行为:

yq 'with(.a.deeply.nested; . = "newValue" | . style="single")[]' sample.yml

该表达式会先执行with更新,再对其结果做数组化展开,适合在管道中继续链式处理。

与相关操作符的辨析

  • 直接赋值链with是"批量更新"的语法糖,等价于把更新表达式逐条写在完整路径上,但更简洁、路径只需维护一次;
  • with_entries(别名withEntries:在 lexer_participle.go 中与with相邻注册,但它是另一个操作符,用于"对 map 的每个键值对(entry)执行更新后再组装回 map",语义上偏向 entries 变换,而with是通用路径上下文切换,两者不要混淆;
  • map操作符map也是逐元素应用表达式(operation.go),但map的表达式结果会作为新值替换元素,而with的第二个表达式是在原节点上做原地更新(赋值、改样式等),适合"改属性"而非"生成新值"。

测试验证

with的全部行为都由 operator_with_test.go 中的场景化测试覆盖,共包含 5 个用例:

场景表达式要点
Update and stylewith(.a.deeply.nested; . = "newValue" \| . style="single")更新值并改样式
with splat同上追加[]验证后置遍历
Update multiple deeply nested propertieswith(.a.deeply; .nested = ... \| .other= ...)批量更新子属性
Update array elements relativelywith(.myArray[]; .b = .a + " yum")逐元素相对更新
Update array elements relatively +=with(.myArray[]; .a += .a)复合赋值

这些用例同时也是文档示例的直接证据来源,运行go test ./pkg/yqlib/ -run TestWithOperatorScenarios即可在本地复现全部行为。

小结

with操作符通过"路径表达式 + 更新表达式块"的两段式结构,把复杂的深层路径更新收敛成一段自包含的逻辑,并借助"单节点上下文"的遍历机制天然支持数组元素的相对更新。无论是修改深层嵌套字段、批量赋值,还是按元素计算新属性,with都能显著提升 yq 表达式的可读性与可维护性。更多操作符的完整索引可参考 operators 文档目录,with的权威说明与示例则始终以 with.md 为准。

【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询