yq flatten 操作符完全指南:递归展平嵌套数组的原理与实战
2026/9/14 13:16:22 网站建设 项目流程

yq flatten 操作符完全指南:递归展平嵌套数组的原理与实战

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

flatten是 yq 中用于将嵌套数组递归展平的专用操作符,它能把多层嵌套的序列结构拍平为一层,并支持通过flatten(N)精确控制展平深度。本文以仓库文档 pkg/yqlib/doc/operators/flatten.md 为主线,结合 operator_flatten.go 的源码实现与 operator_flatten_test.go 的测试用例,系统讲解其语法、默认行为、深度控制、边界情况,以及处理对象数组等典型场景,帮助你彻底掌握在 yq 中处理嵌套数组的能力。

flatten 是什么

flatten是一个递归展平数组的操作符。文档开篇即给出其定义:"This recursively flattens arrays"(递归展平数组)。它的作用是把嵌套的数组序列展开,消除层级结构,让所有元素平铺在同一个数组层级中。

在 yq 中,flatten有两种调用形式:

语法含义
flatten递归展平所有层级的数组,直至完全展平
flatten(N)仅展平到指定深度 N(N 为非负整数)

从词法分析器的定义可以确认这两种形式:

// pkg/yqlib/lexer_participle.go {"FlattenWithDepth", `flatten\([0-9]+\)`, flattenWithDepth(), 0}, {"Flatten", `flatten`, opTokenWithPrefs(flattenOpType, nil, flattenPreferences{depth: -1}), 0},

不带参数时,flatten被解析为flattenPreferences{depth: -1}-1表示不限制深度(无限递归);带参数时,则通过flattenWithDepth()解析括号内的数字并存入flattenPreferences{depth: depth}

从操作符注册表看,flatten的完整定义位于 pkg/yqlib/operation.go#L197:

var flattenOpType = &operationType{Type: "FLATTEN_BY", NumArgs: 0, Precedence: 52, Handler: flattenOp, CheckForPostTraverse: true}

该操作符的处理器为flattenOp,且标记了CheckForPostTraverse: true,意味着它在执行后会触发后续遍历(也就是测试中出现的flatten[]分裂形式,详见下文"与 splat 操作符的组合")。

基本用法:递归展平所有层级

示例:完全展平

文档给出了第一个也是最核心的示例。假设sample.yml内容如下:

- 1 - - 2 - - - 3

即顶层是一个数组,包含元素1、子数组[2]、孙数组[[3]]。执行:

yq 'flatten' sample.yml

输出结果为:

- 1 - 2 - 3

可以看到,嵌套在两层数组中的23都被提取出来,与1平级排列。

对应的测试用例在 pkg/yqlib/operator_flatten_test.go#L7-L16:

{ description: "Flatten", subdescription: "Recursively flattens all arrays", document: `[1, [2], [[3]]]`, expression: `flatten`, expected: []string{ "D0, P[], (!!seq)::[1, 2, 3]\n", }, },

测试用 JSON 形式[1, [2], [[3]]]验证了同样结论:结果为[1, 2, 3],展平后的节点仍然是序列(!!seq)。

示例:展平空数组

文档还专门验证了一个边界情况——当数组中包含空数组时。假设sample.yml内容为:

- []

执行:

yq 'flatten' sample.yml

输出结果为:

[]

也就是说,[[]]展平后得到[]——空数组被展平后不会留下任何残留元素,结果是一个空数组。对应测试 pkg/yqlib/operator_flatten_test.go#L48-L54:

{ description: "Flatten empty array", document: `[[]]`, expression: `flatten`, expected: []string{ "D0, P[], (!!seq)::[]\n", }, },

示例:展平对象数组

flatten不仅适用于纯标量数组,对包含对象的数组同样有效。假设sample.yml内容为:

- foo: bar - - foo: baz

顶层数组由对象{foo: bar}和嵌套数组[{foo: baz}]组成。执行:

yq 'flatten' sample.yml

输出结果为:

- foo: bar - foo: baz

嵌套数组中的对象{foo: baz}被提升到顶层,两个对象平级排列。对应测试 pkg/yqlib/operator_flatten_test.go#L56-L62:

{ description: "Flatten array of objects", document: `[{foo: bar}, [{foo: baz}]]`, expression: `flatten`, expected: []string{ "D0, P[], (!!seq)::[{foo: bar}, {foo: baz}]\n", }, },

控制展平深度:flatten(N)

有时我们并不想把数组完全展平,而只希望消除一层或几层嵌套。flatten(N)允许你指定展平深度。

示例:只展平一层

文档示例中,sample.yml内容为:

- 1 - - 2 - - - 3

执行:

yq 'flatten(1)' sample.yml

输出结果为:

- 1 - 2 - - 3

与完全展平的结果对比:2[2]中被提取出来,但3仍保留在嵌套数组[3]中——因为[[3]]需要两次展平才能把3提到顶层,而flatten(1)只做了一层。

对应测试 pkg/yqlib/operator_flatten_test.go#L29-L35:

{ description: "Flatten with depth of one", document: `[1, [2], [[3]]]`, expression: `flatten(1)`, expected: []string{ "D0, P[], (!!seq)::[1, 2, [3]]\n", }, },

深度参数的限制

从词法规则可以看出,flatten(N)中的 N 必须是非负整数

// pkg/yqlib/lexer_participle.go {"FlattenWithDepth", `flatten\([0-9]+\)`, flattenWithDepth(), 0},

正则flatten\([0-9]+\)只匹配十进制数字,因此不能传入负数、小数或变量表达式。不过,extractNumberParameter的正则本身支持负数(-?[0-9]+),只是词法层面已经用[0-9]+做了约束。如果不带参数直接写flatten,深度会被设为-1,语义是"无限深度",即完全展平。

extractNumberParameter的实现位于 pkg/yqlib/lexer.go#L63-L71,它从形如flatten(1)的原始 token 中提取括号内的数字:

func extractNumberParameter(value string) (int, error) { parameterParser := regexp.MustCompile(`.*\((-?[0-9]+)\)`) matches := parameterParser.FindStringSubmatch(value) var indent, errParsingInt = parseInt(matches[1]) ... return indent, nil }

底层实现原理

flatten的核心逻辑在 pkg/yqlib/operator_flatten.go 中,由flattenOp处理器和递归辅助函数flatten两部分组成。

操作符入口 flattenOp

func flattenOp(_ *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { log.Debugf("flatten Operator") depth := expressionNode.Operation.Preferences.(flattenPreferences).depth for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) if candidate.Kind != SequenceNode { return Context{}, fmt.Errorf("only arrays are supported for flatten") } flatten(candidate, depth) } return context, nil }

要点如下:

  1. 深度偏好(flattenPreferences.depth)由词法分析阶段写入表达式节点的Operation.Preferences,执行时直接取出。
  2. 遍历当前上下文中所有匹配节点,对每个匹配的候选节点执行展平。
  3. 类型检查:如果候选节点不是序列(SequenceNode,即数组),会直接返回错误only arrays are supported for flatten——也就是说flatten只能作用于数组,不能对映射(map)或标量执行。

递归展平函数 flatten

func flatten(node *CandidateNode, depth int) { if depth == 0 { return } if node.Kind != SequenceNode { return } content := node.Content newSeq := make([]*CandidateNode, 0) for i := 0; i < len(content); i++ { if content[i].Kind == SequenceNode { flatten(content[i], depth-1) for j := 0; j < len(content[i].Content); j++ { newSeq = append(newSeq, content[i].Content[j]) } } else { newSeq = append(newSeq, content[i]) } } node.Content = make([]*CandidateNode, 0) node.AddChildren(newSeq) }

这个递归函数揭示了几条关键语义:

  • 深度为 0 时直接返回:这是递归的终止条件,也是flatten(1)只展平一层的原因——对顶层数组深度为 1,展开一层后递归进入子数组时深度变为 0,停止继续展开。
  • 只处理序列节点:遇到非序列元素(标量、映射)原样保留。
  • 就地替换:遍历原内容,遇到序列子元素就先递归展平它,然后把它的全部子元素平铺进newSeq;非序列元素直接追加。最后清空原node.Content并用AddChildren(newSeq)重建,实现就地展平。
  • 无限深度的实现:不带参数时depth = -1,由于depth == 0永远不会触发(每次递归都做depth-1,从 -1 一路递减),递归会一直进行到所有嵌套序列都被展开为止。

从 pkg/yqlib/operation.go#L197 可以看到flattenOpType声明了Precedence: 52,在操作符优先级表中处于较高位置,这保证了flatten与其他操作符组合时拥有确定的求值顺序。

与 splat 操作符的组合(flatten[])

源码中还记录了flatten与 splat([])组合的特殊形态,虽然文档正文未展开,但测试给出了明确语义。例如:

// pkg/yqlib/operator_flatten_test.go { description: "Flatten splat", skipDoc: true, document: `[1, [2], [[3]]]`, expression: `flatten[]`, expected: []string{ "D0, P[0], (!!int)::1\n", "D0, P[1], (!!int)::2\n", "D0, P[2], (!!int)::3\n", }, },

flatten[]会先展平数组,再把展平后的每个元素分裂成独立节点输出(对应CheckForPostTraverse: true的语义)。带深度参数的形态flatten(1)[]则只分裂一层展平后的结果:

{ description: "Flatten with depth and splat", skipDoc: true, document: `[1, [2], [[3]]]`, expression: `flatten(1)[]`, expected: []string{ "D0, P[0], (!!int)::1\n", "D0, P[1], (!!int)::2\n", "D0, P[2], (!!seq)::[3]\n", }, },

从源码结构看,这种"展平后逐元素输出"的行为可以用于对数组每个元素继续施加后续管道操作,是批量处理嵌套数组的实用技巧。这两个用例在测试中标记了skipDoc: true,说明它们是源码内部验证的组合场景,但完全可以在你的表达式中直接使用。

实战建议与注意事项

处理非数组输入的报错

由于flattenOp会检查节点类型,对非数组输入会返回only arrays are supported for flatten错误。因此在使用前应确认目标节点是数组,例如先通过typetag操作符判断类型,或确保查询路径一定指向数组字段。

深度参数的选取

  • 需要完全拍平多层嵌套时,直接用flatten
  • 只需要消除一层容器结构(例如把"数组的数组"转成"数组")时,用flatten(1),可以避免误伤更深层的合法嵌套。
  • 深度参数必须是字面量非负整数,不能用变量或表达式代替(词法层flatten\([0-9]+\)的限制)。

与其他操作符的管道组合

由于flatten返回展平后的数组,你可以把它接在管道中继续处理,例如:

yq '[.a[]] | flatten | unique' sample.yml yq '.matrix.include | flatten(1) | map(.name)' sample.yml

(具体组合取决于你的数据结构,flatten的输出始终是序列,可作为后续操作符的输入。)

测试验证

仓库在 pkg/yqlib/operator_flatten_test.go 中通过TestFlattenOperatorScenarios覆盖了本文所述全部场景:完全展平、深度展平、空数组、对象数组、splat 组合,并同步生成文档用例(documentOperatorScenarios(t, "flatten", flattenOperatorScenarios))。你在修改或验证自己表达式时,可以参考这些用例组织测试数据。

小结

flatten是 yq 处理嵌套数组的核心操作符:无参形式递归展平全部层级,flatten(N)精确控制展平深度,空数组与对象数组都能正确处理。其底层实现在 operator_flatten.go 中通过递归 + 深度计数实现就地展平,词法层在 lexer_participle.go 中区分flattenflatten(N)两种语法,并有完整的 测试用例 保证行为稳定。掌握它的默认语义、深度控制与类型约束,你就能在各种 YAML、JSON 数据处理场景中高效地整理嵌套数组。

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

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询