news 2026/9/14 18:54:47

yq Recipes 实战指南:用组合操作符完成复杂数据查询、更新与自定义格式导出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
yq Recipes 实战指南:用组合操作符完成复杂数据查询、更新与自定义格式导出

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)

目标:从顶层数组中找出nameFoo的元素。

给定sample.yml

- name: Foo numBuckets: 0 - name: Bar numBuckets: 0

命令:

yq '.[] | select(.name == "Foo")' sample.yml

输出:

name: Foo numBuckets: 0

原理拆解:

  1. .[]把数组 splat 开,每个元素进入上下文;
  2. 元素经|送入select(.name == "Foo"),只保留name等于Foo的节点。

2.2 查找并更新数组元素(find + update)

目标:把nameFoo的元素中的numBuckets自增 1。

输入文件同上,命令:

yq '(.[] | select(.name == "Foo") | .numBuckets) |= . + 1' sample.yml

输出:

- name: Foo numBuckets: 1 - name: Bar numBuckets: 0

原理拆解:

  1. .[]splat 数组,select过滤出目标元素;
  2. 再经|选到.numBuckets字段;
  3. 括号内的整体表达式作为|=的左操作数,. + 1作为右操作数——|=表示“基于自身当前值更新”,当前值即.
  4. . + 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)

目标:文档树中只保留child1child2,其余节点全部丢弃,但保持原有路径结构

给定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

原理拆解(这也是仓库中最有“组合艺术感”的配方):

  1. 找节点..递归下降遍历所有节点(递归下降实现在 operator_recursive_descent.go),select(has("child1") or has("child2"))匹配“拥有 child1 或 child2 的父节点”(has见 operator_has.go),再用(.child1, .child2)联合选出这两个子节点,select(.)过滤掉不存在的null
  2. 重建文档as $i ireduce({}; ...)从空 map 开始,按顺序把每个匹配节点setpath($i | path; $i)它原本的路径上,从而拼出一棵只含目标节点、且目录层级完整的新树;
  3. ireduce是有序 reduce 变体,与reduce同源于 operator_reduce.go 的reduceOperatorpath操作符在 operator_path.go 中提供节点到根的路径序列,是“按原路径回填”的关键。

3.2 过滤、扁平化、排序、去重(Filter, flatten, sort and unique)

目标:从混合文档中取出typefoo的所有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

原理拆解,按管道四段看:

  1. .[] | select(.type == "foo") | .names:splat 顶层数组、过滤出foo类型元素、取它们的names
  2. 注意外层方括号[...]:splat 之后结果不再是数组,用方括号把所有匹配值重新收集回一个数组,此时它是一个“数组里混着字符串和子数组”的嵌套结构;
  3. flatten(operator_flatten.go)把嵌套数组拍平成一维字符串列表;
  4. 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
  • 环境变量脚本的两个表达式(bashEnvScriptnestedBashEnvScript)分别定义了期望的三行/四行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\|=
循环变换按兄弟字段改 namewith
结构化重建深度剪枝..ireducesetpathpath
聚合与文本生成排序去重、脚本导出[...]flattensortuniquekindjoinunion

掌握“展开 → 过滤 → 变换 → 收集/回写”这一心智模型后,再复杂的需求也能拆成若干行管道。每个操作符的完整参数与边界行为,可在仓库的 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 18:54:20

HoRain云--FastAPI 从入门到生产:路由、依赖注入、异步与部署

1. 为什么选择 FastAPIFastAPI 基于 Starlette 和 Pydantic,支持异步、自动生成 OpenAPI 文档、类型提示驱动开发。性能接近 Node.js 和 Go,开发效率极高。安装:bash复制下载pip install fastapi uvicorn[standard]2. 第一个接口python复制下…

作者头像 李华
网站建设 2026/9/14 18:53:31

JWT在分布式系统中的认证实践与优化

1. 分布式系统中的登录挑战在分布式架构成为主流的今天,登录认证面临着前所未有的复杂性。想象一下:你的用户在北京访问了部署在上海的服务器A完成登录,下一秒却需要从广州的服务器B获取数据。传统的session-cookie机制在这种场景下会暴露出明…

作者头像 李华
网站建设 2026/9/14 18:52:43

深度优先算法入门级例题

46. 全排列 文章目录[46. 全排列](https://leetcode.cn/problems/permutations/)- 递归枚举- 回溯法结语给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1: 输入:nums [1,2,3] 输出&…

作者头像 李华
网站建设 2026/9/14 18:51:19

PHP开源发布站系统:高效内容管理与二次开发指南

1. 项目概述:PHP开源发布站系统的核心价值这套PHP开源发布站系统源码是当前内容分发领域的一把瑞士军刀。作为一名经历过多个发布站项目的老兵,我深刻理解这类系统的核心价值——它不仅是一个简单的文章发布工具,更是构建内容生态的基础设施。…

作者头像 李华