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 the
withoperator 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操作符用于解决两个典型问题:
- 对深层嵌套路径做多处更新:无需反复书写冗长的路径前缀;
- 让数组元素之间进行相对更新:第二个表达式会以数组的每一个元素作为上下文根节点运行,因此可以在表达式内部用
.引用元素自身,实现"相对于元素"的更新。
其通用语法为:
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:与map、filter、omit、eval等操作符同级,属于高优先级表达式;CheckForPostTraverse: true:允许在with(...)之后继续追加后置遍历操作(例如with(...)[]这种 splat 写法,见下文测试用例);NumArgs: 1:with本身只接收一个参数,第二个表达式通过分号块(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 }整个执行流程可以拆解为四步:
- 校验块结构:
with的第二个参数必须是分号块(;),否则直接报错with must be given a block (;)。这保证了第一个参数负责"定位"、第二个参数负责"更新"的语法约束; - 定位目标节点:对第一个路径表达式调用
GetMatchingNodes(定义见 data_tree_navigator.go),得到所有匹配的候选节点集合updateContext; - 逐个执行更新:遍历
updateContext.MatchingNodes中的每一个候选节点,通过SingleChildContext(candidate)(定义见 context.go)把该节点包装成一个只包含单个节点的新上下文,然后以它为根节点执行第二个更新表达式; - 返回原上下文:最终返回进入时的
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 中的样式赋值(style、tag、anchor、comment等)由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 yumwith的循环逐个处理数组元素(对应源码第 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: true,with(...)之后可以继续追加[]等后置遍历操作。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 style | with(.a.deeply.nested; . = "newValue" \| . style="single") | 更新值并改样式 |
| with splat | 同上追加[] | 验证后置遍历 |
| Update multiple deeply nested properties | with(.a.deeply; .nested = ... \| .other= ...) | 批量更新子属性 |
| Update array elements relatively | with(.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),仅供参考