yq ireduce 操作符详解将集合归约为新形式的语法、示例与源码实现【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq本文围绕 yq 的ireducereduce操作符展开先讲清其exp as $name ireduce (init; block)语法中左右两侧各部分的确切语义再通过求和、合并多文件、数组转对象三个完整可运行的示例掌握实战用法最后结合 operator_reduce.go 的实现源码剖析该操作符从表达式校验到迭代累积的底层执行链路。读完后你可以用ireduce在 yq 中完成聚合、跨文档合并与自定义归约逻辑并理解 yq 内置的array_to_map等高级操作符是如何用ireduce实现的。1. 语法总览一个表达式分几部分reduce是把一组数据集合处理成某种新形式单个归约结果的强有力方式。其完整语法为exp as $name ireduce (init; block)例如.[] as $item ireduce (0; . $item)语法各部分的语义如下左侧LHSexp as $name这是一个变量赋值表达式用于指定待归约的元素集合同时为集合中的每一个元素命名$name。例如.[] as $item中.[]会把数组打散splat为各个独立元素元素依次被绑定到变量$item上。右侧RHS(init; block)这是一个 block块表达式包含两部分以分号分隔init累加器accumulator的初始值block针对集合中每个元素更新累加器的表达式。关键点在 block 内部.始终指向当前累加器的值而集合中的元素则通过你命名的变量如$item访问。从源码结构看这个左赋值、右块的结构不是约定俗成的写法而是被解析器与操作符共同强制校验的语法约束具体见第 5 节。2. 为什么叫 ireduce 而不是 reduceyq 的 reduce 语法与jq略有不同。yq目前不如jq那样复杂它只支持中缀infix记法——例如a b操作符位于两个参数中间而jq混用了中缀与**前缀prefix**记法jq中的reduce a b相当于写作 a b。因此为了向后兼容——若将来 yq 添加jq风格的前缀版本reduce当前这个中缀形式就不会与之冲突——该操作符被命名为ireduce。这也是 reduce.md 原文明确给出的设计动机i前缀代表 infix 中缀形式的 reduce。在词法分析层面ireduce被注册为一个二元操作符两个操作数左侧变量赋值表达式、右侧 block 表达式。见 operation.govar reduceOpType operationType{Type: REDUCE, NumArgs: 2, Precedence: 35, Handler: reduceOperator}其中NumArgs: 2与上文语法的左右两部分一一对应对应的词法 token 定义在 lexer_participle.gosimpleOp(ireduce, reduceOpType),3. 示例一对数字求和给定 sample.yml- 10 - 2 - 5 - 3执行yq .[] as $item ireduce (0; . $item) sample.yml输出20逐步解读.[]把顶层数组打散为10、2、5、3四个候选元素累加器初始值为0block 中. $item每次都取当前累加值 本元素。四轮迭代后得到20。该场景同样出现在官方测试用例 operator_reduce_test.go 中测试数据为[10,2, 5, 3]断言输出为20带!!int类型标签保证上述行为在当前代码库中是被持续验证的实现事实。4. 示例二用 eval-all 合并所有 YAML 文件给定 sample.ymla: cat以及 another.ymlb: dog执行yq eval-all . as $item ireduce ({}; . * $item ) sample.yml another.yml输出a: cat b: dog要点这里用eval-all命令加载所有输入文档LHS 的.会依次匹配到a: cat与b: dog两个完整文档累加器初始化为空对象{}block 中使用合并操作符*multiply/merge见 multiply-merge.md将当前文档合并进累加器因此多个文件的顶层键值被逐层叠加成一份文档。该场景同样有测试覆盖operator_reduce_test.go 中 document 为a: cat、document2 为b: dog的双文档用例期望输出即a: cat\nb: dog。5. 示例三把数组转换为对象给定 sample.yml- name: Cathy has: apples - name: Bob has: bananas执行yq .[] as $item ireduce ({}; .[$item | .name] ($item | .has) ) sample.yml输出Cathy: apples Bob: bananas解读累加器初始为空对象每一轮迭代中.[$item | .name] ($item | .has)以当前元素的name作为键、has作为值向累加器中写入一个键值对。两轮之后累加器就是一份以 name 为键的对象。这是ireduce最典型的用法模式用空 map 作初始值在 block 里不断向累加器写键从而把集合重塑为映射结构。6. 底层实现operator_reduce.go 的执行链路上面三个例子在 yq 库中统一由 operator_reduce.go 里的reduceOperator函数处理。其执行链路可以归纳为五步与上文语法逐条对应第一步强制校验 LHS 是变量赋值、RHS 是 block。if expressionNode.LHS.Operation.OperationType ! assignVariableOpType { return Context{}, fmt.Errorf(reduce must be given a variables assignment, got %v instead, ...) } else if expressionNode.RHS.Operation.OperationType ! blockOpType { return Context{}, fmt.Errorf(reduce must be given a block, got %v instead, ...) }也就是说如果你写出ireduce却不是变量赋值 块的组合比如1 ireduce (0; . 1)yq 会直接报reduce must be given a variables assignment或reduce must be given a block错误。这解释了为什么文档中强调左侧是变量赋值操作符、右侧必须是块block。第二步求出待归约的候选节点集合。arrayExpNode : expressionNode.LHS.LHS // exp如 .[] array, err : d.GetMatchingNodes(context, arrayExpNode)expressionNode.LHS.LHS即赋值表达式左边真正的集合表达式exp通过dataTreeNavigator的GetMatchingNodes求出所有匹配节点LHS 变量名从expressionNode.LHS.RHS中取出如$item。第三步求出累加器的初始值。initExp : expressionNode.RHS.LHS accum, err : d.GetMatchingNodes(context, initExp)即对(init; block)中分号前半段init求值作为起始累加器示例中的0、{}。第四步逐元素迭代绑定变量并更新累加器。for el : array.MatchingNodes.Front(); el ! nil; el el.Next() { candidate : el.Value.(*CandidateNode) l : list.New() l.PushBack(candidate) accum.SetVariable(variableName, l) // 把当前元素绑定到 $item accum, err d.GetMatchingNodes(accum, blockExp) // 以累加器为上下文执行 block ... }注意两个细节集合按节点列表的原始顺序Front()到Next()从左到右依次迭代因此归约结果与元素顺序有关——例如示例二中sample.yml在前、another.yml在后每轮迭代把当前候选元素包装成单元素列表后SetVariable到累加器上下文再以累加器自身为上下文.对 block 重新求值求值结果替换累加器进入下一轮。这正是block 内.表示当前累加器值这一语义的代码级来源。第五步返回最终累加器。循环结束后return accum, nil归约完成。7. ireduce 是 yq 的积木内置操作符与官方配方ireduce不仅面向用户也是 yq 内部其他能力的基础构建块。array_to_map 用 ireduce 实现。array-to-map.md 明言其在幕后使用 reduce 实现而 lexer_participle.go 中该内置操作符的定义就是一行现成的ireduce表达式{ArrayToMap, array_?to_?map, expressionOpToken((.[] | select(. ! null) ) as $i ireduce({}; .[$i | key] $i)), 0},对照第 5 节的示例三模式完全一致select(. ! null)先过滤空值然后以{}为初始值逐元素执行.[$i | key] $i把索引作键、元素作值写入累加器。这为阅读ireduce提供了官方背书的可信范本。官方配方中的深度裁剪Deeply prune a tree。recipes.md 中给出了一个组合多个操作符、最终用ireduce归约的完整配方在复杂树中只保留child1/child2按原始路径重建文档yq ( .. | # 递归遍历所有节点 select(has(child1) or has(child2)) | # 匹配拥有 child1 或 child2 的父节点 (.child1, .child2) | # 选中这些子节点 select(.) # 过滤掉 null ) as $i ireduce({}; # 用这一组节点构建新的结果 map setpath($i | path; $i) # 按节点原始路径逐一放回 ) sample.yml思路与第 5 节示例三同源先选出目标节点集合再用ireduce({}; ...)把空 map 逐步长出只含目标路径的新结构setpath($i | path; $i)利用节点自带的path信息把每个节点放回原位置。8. 使用要点与适用前提小结语法契约exp as $name ireduce (init; block)中左侧必须是变量赋值、右侧必须是;分隔的两段块否则直接报错见第 6 节源码.语义block 内.是当前累加器不是当前元素当前元素通过$name访问集合的展开若 LHS 是.[]数组会被打散为独立元素参与归约若是.如eval-all场景则每个匹配文档各参与一轮迭代顺序性迭代按匹配节点顺序进行归约结果依赖输入顺序适用前提以上内容基于当前代码库yq 项目pkg/yqlib包中的 reduce.md、operator_reduce.go 及 operator_reduce_test.go 实际内容示例命令均可按当前仓库版本直接复制运行ireduce的中缀设计若未来新增jq式前缀reduce将保持向后兼容使用时无需担心既有写法失效。【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
