
yq 的 with 操作符实战指南深层路径批量更新与数组元素相对更新【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yqwith是 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操作符用于解决两个典型问题对深层嵌套路径做多处更新无需反复书写冗长的路径前缀让数组元素之间进行相对更新第二个表达式会以数组的每一个元素作为上下文根节点运行因此可以在表达式内部用.引用元素自身实现相对于元素的更新。其通用语法为yq with(路径表达式; 更新表达式块) 文件注意两个参数之间使用分号;连接这在 yq 中表示一个块block。第一个参数是路径定位表达式第二个参数可以是任意由|或;串联起来的更新表达式。从源码看 with 的执行流程语法注册与优先级with在词法层面由 lexer_participle.go 注册为普通操作符simpleOp(with, withOpType)其操作类型定义在 operation.govar withOpType operationType{Type: WITH, NumArgs: 1, Precedence: 52, Handler: withOperator, CheckForPostTraverse: true}Precedence: 52与map、filter、omit、eval等操作符同级属于高优先级表达式CheckForPostTraverse: true允许在with(...)之后继续追加后置遍历操作例如with(...)[]这种 splat 写法见下文测试用例NumArgs: 1with本身只接收一个参数第二个表达式通过分号块block传入这一点在下面的实现中会看到。分号块;本身由 lexer_participle.go 映射为blockOpType定义于 operation.govar blockOpType operationType{Type: BLOCK, Precedence: 10, NumArgs: 2, Handler: emptyOperator}也就是说with(A; B)在解析后with的右子树RHS是一个 BLOCK 节点其 LHS 是路径表达式 ARHS 是更新表达式 B。withOperator 的实现原理核心实现位于 operator_with.gofunc 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.ymla: deeply: nested: value执行yq with(.a.deeply.nested; . newValue | . stylesingle) sample.yml输出a: deeply: nested: newValue这里第二个表达式块中发生了两件事. newValue把上下文根节点即.a.deeply.nested的值赋值为newValue. stylesingle对该节点设置 YAML 样式属性为单引号。yq 中的样式赋值style、tag、anchor、comment等由assignStyleOperator/assignAttributesOperator一族操作符处理见 operation.go样式设置细节可参考 style.md。注意.在这里代表上下文根节点本身而不是当前文档根这正是with重新设置 root context 的直观体现。场景二批量更新多个深层嵌套属性如果目标是一个深层 map想要同时更新它的多个子属性with可以把路径前缀只写一次。给定sample.ymla: 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.ymlmyArray: - 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: truewith(...)之后可以继续追加[]等后置遍历操作。operator_with_test.go 中的 with splat 场景验证了这一行为yq with(.a.deeply.nested; . newValue | . stylesingle)[] 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 \| . stylesingle)更新值并改样式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),仅供参考