yq Pipe 管道操作符:把一个表达式接进下一个表达式
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
yq 是一款可移植的 YAML、JSON、XML、CSV、TOML、HCL 与 properties 文件处理器,它的查询语言中最核心的“粘合剂”就是管道(Pipe)操作符|。本篇基于仓库文档 pipe.md 展开:先完整覆盖文档中的简单管道与连续更新两个用法,再结合 yq 的词法解析、操作符注册与求值器源码,讲清楚|的优先级、求值时机与上下文传递机制,帮助你写出可预测、可组合的 yq 表达式。
什么是 Pipe:把表达式的结果喂给下一个表达式
Pipe 的行为与 Bash 中的|类似:左边表达式的匹配结果,成为右边表达式的输入上下文。在 yq 中,这个“结果”是一组候选节点(candidate nodes)——既可能是值本身,也可能是带有路径信息的节点(这样赋值操作才能写回原文档)。
官方文档对 Pipe 的一句话定义是:“Pipe the results of an expression into another. Like the bash operator.”(把一个表达式的结果传给另一个表达式,就像 Bash 操作符一样)。
简单管道:逐级深入取值
文档给出的第一个例子:给定一个sample.yml文件:
a: b: cat执行:
yq '.a | .b' sample.yml输出:
cat执行过程可以拆解为两步:
.a在文档上求值,得到节点b: cat这个映射子树(路径a);- 管道把该子树作为新的输入上下文,
.b在子树上求值,得到标量cat。
也就是说,.a | .b等价于写.a.b,但拆成两段管道后,你可以在中间插入任意变换操作,这正是管道表达力的来源。例如在包含数组的数据上:
yq '.a[] | select(.b == "cat")' sample.yml # 先展开数组,再过滤管道两侧的表达式各自独立求值,右侧表达式的起点是左侧的终点,这使 yq 表达式天然适合链式组合(select、map、sort等单目/遍历操作符都常与|搭配使用,相关文档见 select.md、map.md)。
连续更新:管道串起多个赋值
文档的第二个例子展示了管道在“写”方向的价值。给定sample.yml:
a: cow b: sheep c: same执行:
yq '.a = "cat" | .b = "dog"' sample.yml输出:
a: cat b: dog c: same三个要点:
- 从左到右依次执行:
.a = "cat"先完成,文档状态更新后,.b = "dog"在此基础上执行,最后c: same未被触及而原样保留; - 每条更新都作用于同一份文档:与“读取结果再传递”不同,赋值类操作直接修改文档树,管道只是规定了操作的先后顺序;
- 更新表达式可以很长:把一次复杂的文档变换拆成多个以
|分隔的小步骤,是 yq 表达式可读性的关键实践。
仓库中的测试用例 operator_pipe_test.go 精确复现了文档这两个例子:
{ description: "Simple Pipe", document: `{a: {b: cat}}`, expression: `.a | .b`, expected: []string{ "D0, P[a b], (!!str)::cat\n", }, }, { description: "Multiple updates", document: `{a: cow, b: sheep, c: same}`, expression: `.a = "cat" | .b = "dog"`, expected: []string{ "D0, P[], (!!map)::{a: cat, b: dog, c: same}\n", }, },测试中的P[a b]表示最终节点路径为a.b,说明管道确实逐级推进了路径;P[]表示第二个例子的结果是文档根节点本身(更新后输出整份文档)。
变量与管道:as $x |的专门入口
yq 的变量赋值(expr as $x | ...)在求值器层面是走管道分支的。pipe.md 未展开这一点,但源码 operator_pipe.go 中有明确的专门处理:
func pipeOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { if expressionNode.LHS.Operation.OperationType == assignVariableOpType { return variableLoop(d, context, expressionNode) } lhs, err := d.GetMatchingNodes(context, expressionNode.LHS) ... rhsContext := context.ChildContext(lhs.MatchingNodes) rhs, err := d.GetMatchingNodes(rhsContext, expressionNode.RHS) ... return context.ChildContext(rhs.MatchingNodes), nil }当管道左侧是变量赋值(as)时,会转交给 variableLoop 处理;该文件中的注释直接说明设计意图——“variables are like loops in jq”(变量像 jq 中的循环一样,逐值展开执行右侧)。而普通管道则严格按两阶段执行:
GetMatchingNodes(context, LHS)在当前上下文上求值左表达式;context.ChildContext(lhs.MatchingNodes)用左侧匹配结果构造子上下文;- 在子上下文上求值右表达式,其结果成为管道整体输出。
变量操作的约束同样写在代码里:useWithPipe会在未配合管道使用时报错must use with a pipe, e.g. exp as $x | ...,且 RHS 必须是变量名(见 variableLoopSingleChild)。
优先级:管道在表达式中处于哪一层
|的词法识别在 lexer_participle.go 中注册({"Pipe", "\|", opToken(pipeOpType), 0}),其操作符定义在 operation.go:
var pipeOpType = &operationType{Type: "PIPE", NumArgs: 2, Precedence: 30, Handler: pipeOperator}对照 operation.go 中全部操作符的优先级(数字越大结合越紧),管道处于一个关键的分界位置,从低到高大致为:
| 优先级 | 操作符 | 说明 |
|---|---|---|
| 10 | union、块 | a // b(并集)、括号块 |
| 15 | CREATE_MAP | 对象字面量{...}的构建 |
| 20 | OR/AND | 布尔运算or/and |
| 30 | PIPE | 普通管道\| |
| 35 | REDUCE | 归约 |
| 40 | ASSIGN/EQUALS等 | 赋值=、比较== |
| 42 | MULTIPLY、ADD、ALTERNATIVE | *、+、备选//相关运算 |
| 45 | SHORT_PIPE | 集合/对象字面量内部的短管道(如[... | ...]) |
| 50+ | LENGTH、SORT、COLLECT等 | 无参/单参数后缀操作 |
由此得到三条实用的求值规则:
- 赋值高于管道:
.a = "cat" | .b = "dog"被解析为(赋值) | (赋值),而不是.a = ("cat" | .b...)。这就是“连续更新”能成立的原因; - 布尔运算低于管道:
x and y | z中and一侧的表达式会先被管道切开,需要用括号显式控制结合关系; - 算术/备选优先于管道:
"a" + "b" | upper会先完成拼接再进管道。
此外源码中还定义了 shortPipeOpType(优先级 45),它与pipeOpType共用同一个pipeOperator处理器,服务于[... | ...]这类集合内部语法;普通表达式中书写|时对应的是上表的PIPE。
上下文的传递与“只读”保护
理解管道的第二个关键点在于上下文如何跨|传递。每个求值步都携带一个Context(见 context.go),核心字段包括:
type Context struct { MatchingNodes *list.List // 当前匹配到的候选节点 Variables map[string]*list.List // 管道间共享的变量 DontAutoCreate bool // 只读标志:禁止自动创建路径 datetimeLayout string }ChildContext在克隆上下文时复制变量表但保留候选节点引用,源码注释解释了原因:不复制节点才能让ref(引用)操作符正常工作——即左侧表达式如果返回的是文档中真实存在的节点(而非值拷贝),右侧的修改会写回原树。这正是.a | .b = "x"能真正改到a.b的机制。
测试文件中的第三个用例专门验证了“管道不把只读状态泄漏给右侧”:
{ description: "Don't pass readonly context", expression: `(3 + 4) | ({} | .b = "dog")`, expected: []string{ "D0, P[], (!!map)::b: dog\n", }, },左侧(3 + 4)产生的是纯数值结果(对映射求值属于只读语义),但管道右侧{}开启的新对象字面量是全新的可写节点,因此.b = "dog"正常执行并输出b: dog。换句话说:只读保护作用于“读取既有节点”的路径,而不会阻塞在管道右侧构造的新节点上的写入。
小结
回到 pipe.md 的核心内容:
- 读取场景:
.a | .b逐级深入,每段表达式都以上一段的输出为起点; - 更新场景:
.a = "cat" | .b = "dog"从左到右依次修改同一份文档,未涉及的键保持不变; - 实现层面:
PIPE操作符(优先级 30,处理器 pipeOperator)两阶段求值——先算左侧、用ChildContext传递匹配节点、再算右侧;变量as走同一入口但由 variableLoop 逐值展开。
掌握这三层,你就能在任何 yq 表达式中正确预测|的结合与执行顺序,把复杂的查询与批处理改写为清晰、可维护的管道链。
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考