news 2026/9/14 4:15:32

yq 递归下降(Recursive Descent / Glob)操作符 `..` 与 `...` 完全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
yq 递归下降(Recursive Descent / Glob)操作符 `..` 与 `...` 完全指南

yq 递归下降(Recursive Descent / Glob)操作符.....完全指南

【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq

本文以 yq 官方操作符文档 recursive-descent-glob.md 为核心骨架,结合 operator_recursive_descent.go 等源码实现与测试用例,系统讲解 yq 的递归下降操作符..(仅匹配值节点)与...(同时匹配映射键节点)的语法、行为差异、典型实战场景(递归查找、批量改样式、锚点与合并键的遍历边界),并给出大量可直接复制的命令行示例与输入输出对照,帮助你在 YAML/JSON/XML/TOML 等多格式文档中高效地"一把抓取"所有嵌套节点。

yq(portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor)中的递归下降操作符(Recursive Descent,文档中亦称 Glob)可以在不提前知道文档结构的前提下,递归地匹配某个元素下的所有子节点(包括该元素自身),是编写"无论嵌套多深都能生效"的通用过滤、批量修改表达式的最重要武器。它直接对标jq..,并针对 YAML 特性做了扩展,本文会从用法、输出顺序、实现原理到边界行为逐一讲透。

一、操作符概览:.....两种形式

根据官方文档定义:

该操作符递归匹配(或 glob)给定元素的全部子节点,包括该节点自身。最常用于对全部匹配结果递归应用一个过滤器。

yq 提供两种写法:

形式含义典型用途
..仅递归匹配所有值(value)节点,不含映射键(map key)查找/修改特定值
...递归匹配所有值节点 + 映射键节点YAML 中需要对键本身做样式、标签(tag)、锚点/别名处理时

语法层面的实现依据

两种形式在词法分析阶段就被区分开来,见 lexer_participle.go:

{"RecursiveDecentIncludingKeys", `\.\.\.`, recursiveDecentOpToken(true), 0}, {"RecursiveDecent", `\.\.`, recursiveDecentOpToken(false), 0},

recursiveDecentOpToken(includeMapKeys bool)会根据是否包含键来构造不同的遍历偏好(lexer_participle.go):

func recursiveDecentOpToken(includeMapKeys bool) yqAction { prefs := recursiveDescentPreferences{ RecurseArray: true, TraversePreferences: traversePreferences{ DontFollowAlias: true, // 不跟随别名(锚点引用) IncludeMapKeys: includeMapKeys, // .. 为 false,... 为 true }, } return opTokenWithPrefs(recursiveDescentOpType, nil, prefs) }

可以看到:

  • .....对应的是同一个操作符类型RECURSIVE_DESCENT(定义见 operation.go,优先级为 50,无参数),二者的差异完全由IncludeMapKeys这一个偏好开关决定;
  • 两者都设置了DontFollowAlias: true,这正是文档中"别名不会被遍历(Aliases are not traversed)"一节行为背后的源码依据;
  • RecurseArray: true表示数组(序列)中的每个元素也会被递归展开。

操作符的执行入口

核心实现在 operator_recursive_descent.go:

func recursiveDescentOperator(_ *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { var results = list.New() preferences := expressionNode.Operation.Preferences.(recursiveDescentPreferences) err := recursiveDecent(results, context, preferences) ... return context.ChildContext(results), nil } func recursiveDecent(results *list.List, context Context, preferences recursiveDescentPreferences) error { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) results.PushBack(candidate) // 先输出当前节点自身 if candidate.Kind != AliasNode && len(candidate.Content) > 0 && (preferences.RecurseArray || candidate.Kind != SequenceNode) { children, err := splat(context.SingleChildContext(candidate), preferences.TraversePreferences) ... err = recursiveDecent(results, children, preferences) // 深度优先递归 ... } } return nil }

从实现可以看出三个关键点:

  1. 深度优先(DFS)遍历:先输出节点自身,再通过splat展开直接子节点,然后对每个子节点递归调用自身;
  2. 先父后子的输出顺序:因此yq '..'的输出中,父节点总是出现在它的子节点之前;
  3. 别名节点(AliasNode)不会继续展开candidate.Kind != AliasNode条件保证了遍历到*cat这类别名节点时就停止,不会沿着别名指向的锚点继续递归(详见下文第六节)。

二、只匹配值的..:设置所有值节点的样式

用法说明

yq '.. style= "flow"' file.yaml

这条命令会把 YAML 文档中所有值节点的样式设置为 flow(流式风格),但不会修改映射键的样式。这是文档给出的第一个例子,也是最常用的批量修改场景:无论文档嵌套多深、是 map 还是数组,..都能覆盖到。

关于style操作符的更多细节,可参见同目录下的 style 操作符文档。

为什么需要区分值和键?

在 YAML 中(与 JSON 不同),映射键(map key)本身也是一个节点,拥有自己的样式、标签(tag),甚至可以是锚点/别名的目标。例如:

a: frog

这里的a是一个键节点,frog是一个值节点。用..只能命中frog;而键a的样式/标签需要通过...才能操作。

三、同时匹配值 + 键的...:连键一起处理

用法说明

yq '... style= "flow"' file.yaml

..不同,...会把映射键也纳入结果集。文档特别指出:这在 YAML 中尤其有用,因为与 JSON 不同,YAML 的映射键可以有独立的样式、标签,也可以使用锚点(anchors)和别名(aliases)。例如你想统一某个文档所有节点的引号风格、flow/block 风格,就必须用...才能把键节点也覆盖到。

从实现上看,...只是把IncludeMapKeys置为true,在遍历 map 时键和值都会作为候选节点被压入结果列表(见 operator_traverse_path.go 中doTraverseMapIncludeMapKeys的处理)。

四、完整实战示例(含输入输出对照)

下面的示例均来自官方文档并补充了测试依据(见 operator_recursive_descent_test.go 中同名场景),每个场景都给出输入、命令、输出三段,方便直接验证。

4.1 递归 map(仅值形式..

输入sample.yml

a: frog

命令:

yq '..' sample.yml

输出:

a: frog frog

分析:..先输出根节点(整个 mapa: frog),再输出其值节点frog。测试用例对应 operator_recursive_descent_test.go:D0, P[], (!!map)::{a: frog}D0, P[a], (!!str)::frog

4.2 递归查找带指定键的节点

输入sample.yml

a: name: frog b: name: blog age: 12

命令([]只是为了把多个匹配结果收集成数组展示,路径表达式中不包[]也可以):

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

输出:

- name: frog b: name: blog age: 12 - name: blog age: 12

分析:..递归产生所有节点,select(has("name"))过滤出"拥有name键"的节点,命中两个:顶层的a节点(包含name: frog与嵌套的b)和内部的b节点(name: blog, age: 12)。这是一个典型的"递归找对象"模式:不需要知道嵌套深度,直接筛出所有满足条件的节点。测试场景见 operator_recursive_descent_test.go。

4.3 递归查找指定值的节点

输入sample.yml

a: nameA: frog b: nameB: frog age: 12

命令:

yq '.. | select(. == "frog")' sample.yml

输出:

frog frog

分析:递归遍历所有值节点,只保留值等于字符串frog的节点,两处frog都被找到。测试场景见 operator_recursive_descent_test.go,对应的节点路径为P[a nameA]P[a b nameB]

实战提示:把select(. == "frog")换成select(. | type == "~~number")之类的类型判断,或配合 select 操作符文档、filter 操作符文档 中的条件,就可以实现"递归找出所有数字/所有空值/所有匹配正则的节点"等能力。

4.4 递归 map(值 + 键形式...

输入sample.yml

a: frog

命令:

yq '...' sample.yml

输出:

a: frog a frog

分析:注意与 4.1 的差异——多输出了一个a,这正是映射键节点。测试用例见 operator_recursive_descent_test.go:输出顺序为根 map → 键aP[a], (!!str)::a)→ 值frog

对于嵌套结构,...的展开顺序(深度优先、先父后子)可参考测试中的完整断言(operator_recursive_descent_test.go):{a: {b: apple}}会被展开为根节点 →a键 →{b: apple}子 map →b键 →apple值,共 5 个节点。

五、数组(序列)的递归展开

虽然官方文档正文未单独举例,但源码与测试明确了数组在递归下降中的行为:RecurseArray: true意味着数组中的每个元素都会被展开;同时.....对数组而言行为一致(数组没有"键节点"概念)。

以输入[1,2,3]为例(测试见 operator_recursive_descent_test.go):

yq '..' sample.yml

输出(...结果完全相同):

- 1 - 2 - 3 1 2 3

对于混合结构[{a: cat}, 2, true]..会依次输出:根序列 → 第一个 map 元素 → 其键a的值cat2true(见 operator_recursive_descent_test.go)。这也验证了递归下降操作符对 JSON/XML 等以数组为主要容器格式同样有效。

六、边界行为一:别名(Alias)不会被遍历

输入sample.yml

a: &cat c: frog b: *cat

命令:

yq '[..]' sample.yml

输出:

- a: &cat c: frog b: *cat - &cat c: frog - frog - *cat

分析:

  • 根 map 被输出;
  • &cat锚点节点本身(含c: frog)被输出,并继续展开出frog
  • b: *cat中的别名引用*cat只是被作为节点输出(显示为*cat),而不会再沿着别名指向的锚点去递归展开它的内容(即不会重复输出c: frog)。

这正是recursiveDecentcandidate.Kind != AliasNode判断的直接体现(operator_recursive_descent.go):遍历到别名节点时,只把它本身放进结果集,停止继续下钻。测试场景见 operator_recursive_descent_test.go。

七、边界行为二:合并文档(Merge Docs /<<合并键)不会被遍历

输入sample.yml

foo: &foo a: foo_a thing: foo_thing c: foo_c bar: &bar b: bar_b thing: bar_thing c: bar_c foobarList: b: foobarList_b <<: - *foo - *bar c: foobarList_c foobar: c: foobar_c <<: *foo thing: foobar_thing

命令:

yq '.foobar | [..]' sample.yml

输出:

- c: foobar_c <<: *foo thing: foobar_thing - foobar_c - *foo - foobar_thing

分析:对foobar节点递归展开时,<<: *foo这个合并键的值(*foo别名)只是被当作普通节点输出*foo),并不会沿着合并键展开foo锚点中的athingc等内容。也就是说:递归下降不会穿透合并(merge)锚点

从实现角度看,这一行为有两层保障:

  1. 遍历偏好中的DontFollowAlias: true使别名不被跟随(见第一节源码);
  2. recursiveDecentAliasNode的短路判断(operator_recursive_descent.go)。

测试用例见 operator_recursive_descent_test.go;...形式(含键)下的展开结果见同文件 L194-L200,其中合并键<<会作为键节点(tag 为!!merge)出现在结果里。

八、源码视角:操作符优先级与组合使用

RECURSIVE_DESCENT操作符在 operation.go 中定义的优先级为 50,且不带参数:

var recursiveDescentOpType = &operationType{Type: "RECURSIVE_DESCENT", NumArgs: 0, Precedence: 50, Handler: recursiveDescentOperator}

这带来两个实用推论:

  • 可以与管道(|)、过滤(select)自由组合:如.. | select(...).. | has("x")... | . style= "flow"等写法,解析时递归下降操作符会作为管道左侧的节点源;
  • 可以作为赋值(update)表达式的左值:第一节的两个样式设置示例.. style= "flow"... style= "flow"正是"递归下降 + 赋值"的经典组合——左侧../...产出所有目标节点,右侧style=对每个节点做批量修改。若想进一步了解遍历路径与赋值操作符,可参考 traverse-read 操作符文档 与 assign-update 操作符文档。

九、常见问题速查

问题答案
.....有什么区别?..只匹配值节点;...额外匹配映射键节点(YAML 特有需求)。
递归输出顺序是什么?深度优先、先父后子:父节点总是先于其子孙输出(由 operator_recursive_descent.go 的实现顺序保证)。
会不会递归进锚点/别名?不会。别名节点会被输出但不会被展开,合并键<<同样不会被穿透。
数组会被递归吗?会。RecurseArray: true,数组每个元素都会展开;且数组场景下.....行为一致。
空文档/空 map/空数组会怎样?测试表明仅输出根节点自身(见 operator_recursive_descent_test.go 中{}[]cat三个场景)。
从哪里查看全部测试场景?operator_recursive_descent_test.go 中的recursiveDescentOperatorScenariosTestRecursiveDescentOperatorScenarios,覆盖本文所有示例及更多边界输入。

十、总结

递归下降(Recursive Descent / Glob)操作符是 yq 表达式体系中"面向未知结构"的通用遍历能力:

  • ..递归匹配全部值节点,适合批量查找/修改值;
  • .....基础上把映射键也纳入匹配集,适合 YAML 中需要处理键的样式、标签、锚点/别名的场景;
  • 二者共享同一份核心实现(recursiveDescentOperator),差异仅由IncludeMapKeys偏好开关控制,且都默认不跟随别名;
  • 遍历采用深度优先、先父后子顺序,数组元素会被展开,但锚点别名与<<合并文档不会被穿透。

掌握.....,再配合selecthasstyle、赋值等操作符,即可写出"无论文档多深、结构多复杂都能一击命中"的通用 yq 表达式。

延伸阅读(仓库内相关文档)

  • 操作符文档主页:Main.md
  • filter.md:在递归结果上做过滤的常用搭档
  • select.md:配合.. | select(...)实现条件筛选
  • style.md:本文示例中style=赋值操作符详解
  • anchor-and-alias-operators.md:理解锚点/别名节点行为的基础
  • 核心实现:operator_recursive_descent.go、词法定义 lexer_participle.go、操作符注册 operation.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 4:15:24

户外旅游小程序源码改造:从导入到发布的全流程指南

简介&#xff1a;这套户外旅游微信小程序源码专为旅游行业开发者打造&#xff0c;旨在解决景点信息分散、行程规划繁琐、预订流程复杂等常见问题。资源完整包含项目源码、导入视频教程和文档教程&#xff0c;覆盖微信小程序开发基础及旅游类核心功能&#xff0c;所有内容亲测可…

作者头像 李华
网站建设 2026/9/14 4:15:10

K8s Secret实战指南:从创建到安全加固的完整链路

最近帮朋友排查一个线上事故&#xff0c;服务一启动就报数据库连接失败&#xff0c;折腾了半天发现根因不是网络问题&#xff0c;而是他把数据库密码直接写在了Deployment的环境变量里&#xff0c;更麻烦的是这份YAML还被他随手推到了公司Git仓库&#xff0c;开发、预发环境全都…

作者头像 李华
网站建设 2026/9/14 4:15:01

Java遗留系统解析:GB2312编码下JDBC项目结构还原与调试

简介&#xff1a;本资源是一个基于JSP技术实现的携程网功能仿真实验项目&#xff0c;面向Java Web初学者与Web开发入门学习者&#xff0c;旨在通过完整可运行的代码帮助理解在线旅行服务平台的核心业务流程与MVC架构实践。压缩包共128个文件&#xff0c;涵盖20个Java源码&#…

作者头像 李华
网站建设 2026/9/14 4:14:44

QPSK调制解调全流程详解:星座映射、脉冲成形与误码率仿真

简介&#xff1a;一份面向无线通信与信号处理初学者、通信专业学生及相关从业者的 QPSK 调制解调 MATLAB 仿真资源包&#xff0c;覆盖从二进制序列生成、符号映射、载波调制、加噪到解调判决的完整链路&#xff0c;适合通信原理课程实验或入门项目参考&#xff0c;能帮助快速理…

作者头像 李华