yq Recipes 实战指南:用组合操作符完成复杂数据查询、更新与自定义格式导出
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
本文以 yq 仓库内置的 recipes 文档 为核心,完整讲解八个“多操作符组合”的经典配方:从数组元素查找与原地更新、深度剪枝、循环式批量修改、按字段排序,到“过滤 + 扁平化 + 去重”管道和导出 shell 环境变量脚本。每个配方均给出可直接复制运行的完整输入、命令与输出,并结合 yqlib 源码 中对应操作符的实现与 recipes_test.go 的自动化验证,帮助你把 yq 从“查一个字段”提升到“编写可复用数据转换管道”的水平。
一、配方文档的定位与三大基础构件
Recipes(配方)文档的开篇说明:这些示例的目的是演示如何将多个操作符组合起来,以完成复杂的数据操作。单个操作符的完整语义请参考仓库内的操作符文档(如 select 文档、with 文档、assign 文档),本文聚焦“组合技巧”本身。
在展开八个配方面前,先建立三个贯穿全文的构件概念:
- 管道
|:把上游表达式的输出节点交给下游表达式继续处理,是 yq 表达式的基本连接方式; - splat 展开
.[]:把数组“拆开”,让每个数组元素依次进入上下文,这样才能对元素做select过滤、字段访问等“逐元素”操作; - 赋值族操作符:
=(绝对赋值)与|=(基于自身当前值更新,当前值写作.),配合括号表达式可对任意路径做定向修改。
源码层面,管道与展开由表达式引擎统一驱动(expression_parser.go、operators.go),而select的核心实现位于 operator_select.go 的selectOperator——它对每个上下文节点求值过滤条件,为真则保留该节点。理解这一点后,后续所有配方都能拆解为“展开 → 过滤 → 变换 → 收集/回写”四个动作的排列组合。
二、数组配方的四个变体
2.1 在数组中查找元素(find)
目标:从顶层数组中找出name为Foo的元素。
给定sample.yml:
- name: Foo numBuckets: 0 - name: Bar numBuckets: 0命令:
yq '.[] | select(.name == "Foo")' sample.yml输出:
name: Foo numBuckets: 0原理拆解:
.[]把数组 splat 开,每个元素进入上下文;- 元素经
|送入select(.name == "Foo"),只保留name等于Foo的节点。
2.2 查找并更新数组元素(find + update)
目标:把name为Foo的元素中的numBuckets自增 1。
输入文件同上,命令:
yq '(.[] | select(.name == "Foo") | .numBuckets) |= . + 1' sample.yml输出:
- name: Foo numBuckets: 1 - name: Bar numBuckets: 0原理拆解:
.[]splat 数组,select过滤出目标元素;- 再经
|选到.numBuckets字段; - 括号内的整体表达式作为
|=的左操作数,. + 1作为右操作数——|=表示“基于自身当前值更新”,当前值即.; . + 1完成计数器自增。
这一行是 README “Advanced Operations”中 “Find and update an item in an array”(yq -i '(.[] | select(.name == "foo") | .address) = "12 cat st"' data.yaml)的自增版本。从源码结构看,|=的求值路径在 operator_assign.go 中处理:左表达式先定位节点,右表达式以节点自身为.求值后回写;自增所需的加法由 operator_add.go 实现。
2.3 循环式批量更新(with 操作符)
目标:批量改写数组元素的name,且改写内容引用同元素的type字段——这是“一个更新依赖兄弟字段”的典型场景。
给定sample.yml:
myArray: - name: Foo type: cat - name: Bar type: dog命令:
yq 'with(.myArray[]; .name = .name + " - " + .type)' sample.yml输出:
myArray: - name: Foo - cat type: cat - name: Bar - dog type: dog原理拆解:
with操作符对第一个表达式给出的每个元素循环,并在该元素的上下文里执行第二个表达式;.myArray[]展开myArray数组,因此with会对每个元素各执行一次;.name = .name + " - " + .type在每次迭代中运行,把name更新为“原名 + 空格 + 类型”的拼接。
实现位于 operator_with.go 的withOperator:从源码结构看,它先对 LHS 表达式收集出待遍历的节点集合,再逐个以该节点为上下文执行更新块,最后把变更合并回文档——这正是它比手写索引循环更可靠的原因。
2.4 按字段对数组排序(sort_by)
给定sample.yml:
myArray: - name: Foo numBuckets: 1 - name: Bar numBuckets: 0命令:
yq '.myArray |= sort_by(.numBuckets)' sample.yml输出:
myArray: - name: Bar numBuckets: 0 - name: Foo numBuckets: 1原理拆解:
sort_by的工作方式是:输入一个数组(管道进去),输出排序后的数组;- 由于
.myArray是一个字段,用|=原地更新它,等价于写.myArray = (.myArray | sort_by(.numBuckets)); - 排序键
.numBuckets按数值比较,Bar(0) 排在Foo(1) 之前。
sort_by的实现在 operator_sort.go 的sortByOperator中,支持对元素字段排序,也支持负号等键表达式写法(详见该文件及 sort 文档)。
三、两个“管道型”进阶配方
3.1 深度剪枝:只保留 child1 / child2(Deeply prune a tree)
目标:文档树中只保留child1与child2,其余节点全部丢弃,但保持原有路径结构。
给定sample.yml:
parentA: - bob parentB: child1: i am child1 child3: hiya parentC: childX: cool child2: me child2命令:
yq '( .. | # recurse through all the nodes select(has("child1") or has("child2")) | # match parents that have either child1 or child2 (.child1, .child2) | # select those children select(.) # filter out nulls ) as $i ireduce({}; # using that set of nodes, create a new result map setpath($i | path; $i) # and put in each node, using its original path )' sample.yml输出:
parentB: child1: i am child1 parentC: child2: me child2原理拆解(这也是仓库中最有“组合艺术感”的配方):
- 找节点:
..递归下降遍历所有节点(递归下降实现在 operator_recursive_descent.go),select(has("child1") or has("child2"))匹配“拥有 child1 或 child2 的父节点”(has见 operator_has.go),再用(.child1, .child2)联合选出这两个子节点,select(.)过滤掉不存在的null; - 重建文档:
as $i ireduce({}; ...)从空 map 开始,按顺序把每个匹配节点setpath($i | path; $i)到它原本的路径上,从而拼出一棵只含目标节点、且目录层级完整的新树; ireduce是有序 reduce 变体,与reduce同源于 operator_reduce.go 的reduceOperator;path操作符在 operator_path.go 中提供节点到根的路径序列,是“按原路径回填”的关键。
3.2 过滤、扁平化、排序、去重(Filter, flatten, sort and unique)
目标:从混合文档中取出type为foo的所有names,得到排序后的唯一名字列表。
给定sample.yml:
- type: foo names: - Fred - Catherine - type: bar names: - Zelda - type: foo names: Fred - type: foo names: Ava命令:
yq '[.[] | select(.type == "foo") | .names] | flatten | sort | unique' sample.yml输出:
- Ava - Catherine - Fred原理拆解,按管道四段看:
.[] | select(.type == "foo") | .names:splat 顶层数组、过滤出foo类型元素、取它们的names;- 注意外层方括号
[...]:splat 之后结果不再是数组,用方括号把所有匹配值重新收集回一个数组,此时它是一个“数组里混着字符串和子数组”的嵌套结构; flatten(operator_flatten.go)把嵌套数组拍平成一维字符串列表;sort后接unique(operator_unique.go)得到排序后的唯一列表——先去重排序再取唯一,保证结果稳定有序。
这条管道是“收集成数组再处理”模式的代表:先[expr]聚合成数组,再交给面向数组的批量操作符。
四、自定义格式导出:把 YAML 变成 shell 脚本
最后一个配方族展示 yq 的通用能力:表达式可以任意拼字符串,从而导出任意自定义格式,官方示例是生成设置环境变量的 shell 脚本。
4.1 顶层数据导出为环境变量脚本
给定sample.yml:
var0: string0 var1: string1 fruit: - apple - banana - peach命令:
yq '.[] |( ( select(kind == "scalar") | key + "='\''" + . + "'\''"), ( select(kind == "seq") | key + "=(" + (map("'\''" + . + "'\''") | join(",")) + ")") )' sample.yml输出:
var0='string0' var1='string1' fruit=('apple','banana','peach')原理拆解:
.[]匹配所有顶层元素;- 每种节点类型对应一条“生成 bash 语法”的字符串表达式,用**联合(union,
,)**操作符把它们拼成一个表达式; - 标量分支:
select(kind == "scalar") | key + "='" + . + "'",只需键名加带引号的值(kind操作符在 operator_kind.go); - 序列分支较复杂:先
map("'" + . + "'")给每个元素加引号,再join(",")用逗号连接,最后包上(...)得到 shell 数组字面量; - 字符串拼接、
join/map等文本操作由 operator_strings.go 提供。
4.2 嵌套数据的自定义格式(用 path 拼接前缀)
在上一个例子基础上处理嵌套结构,约定用_连接属性路径作为变量名。文档强调的关键点:表达式本身不是递归的(尽管数据是嵌套的),而是用..匹配树上所有元素后统一处理。
给定sample.yml:
simple: string0 simpleArray: - apple - banana - peach deep: property: value array: - cat命令:
yq '.. |( ( select(kind == "scalar" and parent | kind != "seq") | (path | join("_")) + "='\''" + . + "'\''"), ( select(kind == "seq") | (path | join("_")) + "=(" + (map("'\''" + . + "'\''") | join(",")) + ")") )' sample.yml输出:
simple='string0' deep_property='value' simpleArray=('apple','banana','peach') deep_array=('cat')与 4.1 的差异点:
..匹配所有层级的节点,取代了只匹配顶层的.[];- 标量分支新增
and parent | kind != "seq"条件:只打印“父节点不是数组”的标量(数组元素由序列分支统一处理,避免重复);parent操作符实现在 operator_parent.go; - 键名不再用
key,而是path | join("_")取完整路径并以下划线拼接,得到deep_property这类扁平键名; - 序列分支逻辑不变,同样按
path | join("_")生成变量名。
这套“..+kind分流 +path拼键”的模式,可以替换join分隔符和引号风格,派生出 INI、properties、CSV 之外的任意自定义导出格式。
五、配方的自动化验证:文档即测试
以上八个配方并非仅存在于文档中。仓库用 recipes_test.go 把它们全部注册为expressionScenario场景,例如:
- “Find items in an array”:文档为
[{name: Foo, numBuckets: 0}, {name: Bar, numBuckets: 0}],表达式.[] | select(.name == "Foo"),期望输出仅含Foo元素的 map; - “Sort an array by a field”:期望结果为
myArray: [{name: Bar, numBuckets: 0}, {name: Foo, numBuckets: 1}]; - 深度剪枝的期望结果精确到换行与缩进:
parentB:\n child1: i am child1\nparentC:\n child2: me child2\n; - 环境变量脚本的两个表达式(
bashEnvScript、nestedBashEnvScript)分别定义了期望的三行/四行key=value字符串输出。
TestRecipes会先对每个场景执行testScenario断言输出,再调用documentScenarios(t, "usage", "recipes", ...)将场景数据与 recipes 文档 做一致性校验——从源码结构看,这意味着文档中的输入、表达式与预期输出若与测试数据漂移,CI 会失败。这也解释了为什么文档里的每条命令都可以放心直接复制运行。
运行验证(只读方式,不修改仓库文件):
# 查看 recipes 测试代码 cat pkg/yqlib/recipes_test.go # 使用任意 YAML 文件验证本文任一配方,例如: yq '[.[] | select(.type == "foo") | .names] | flatten | sort | unique' examples/data1.yaml六、总结:从配方到可复用管道
八个配方覆盖了 yq 组合表达式的四种基本能力:
| 能力 | 配方 | 核心操作符 |
|---|---|---|
| 逐元素查询/更新 | 查找、自增更新 | .[]、select、\|= |
| 循环变换 | 按兄弟字段改 name | with |
| 结构化重建 | 深度剪枝 | ..、ireduce、setpath、path |
| 聚合与文本生成 | 排序去重、脚本导出 | [...]、flatten、sort、unique、kind、join、union |
掌握“展开 → 过滤 → 变换 → 收集/回写”这一心智模型后,再复杂的需求也能拆成若干行管道。每个操作符的完整参数与边界行为,可在仓库的 operators 文档目录 下按文件名逐一查阅;每个配方的可执行验证依据都在 recipes_test.go 中。
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考