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 }从实现可以看出三个关键点:
- 深度优先(DFS)遍历:先输出节点自身,再通过
splat展开直接子节点,然后对每个子节点递归调用自身; - 先父后子的输出顺序:因此
yq '..'的输出中,父节点总是出现在它的子节点之前; - 别名节点(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 中doTraverseMap对IncludeMapKeys的处理)。
四、完整实战示例(含输入输出对照)
下面的示例均来自官方文档并补充了测试依据(见 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 → 键a(P[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的值cat→2→true(见 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)。
这正是recursiveDecent中candidate.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锚点中的a、thing、c等内容。也就是说:递归下降不会穿透合并(merge)锚点。
从实现角度看,这一行为有两层保障:
- 遍历偏好中的
DontFollowAlias: true使别名不被跟随(见第一节源码); recursiveDecent对AliasNode的短路判断(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 中的recursiveDescentOperatorScenarios及TestRecursiveDescentOperatorScenarios,覆盖本文所有示例及更多边界输入。 |
十、总结
递归下降(Recursive Descent / Glob)操作符是 yq 表达式体系中"面向未知结构"的通用遍历能力:
..递归匹配全部值节点,适合批量查找/修改值;...在..基础上把映射键也纳入匹配集,适合 YAML 中需要处理键的样式、标签、锚点/别名的场景;- 二者共享同一份核心实现(
recursiveDescentOperator),差异仅由IncludeMapKeys偏好开关控制,且都默认不跟随别名; - 遍历采用深度优先、先父后子顺序,数组元素会被展开,但锚点别名与
<<合并文档不会被穿透。
掌握..与...,再配合select、has、style、赋值等操作符,即可写出"无论文档多深、结构多复杂都能一击命中"的通用 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),仅供参考