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可以看到,嵌套在两层数组中的2和3都被提取出来,与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 }要点如下:
- 深度偏好(
flattenPreferences.depth)由词法分析阶段写入表达式节点的Operation.Preferences,执行时直接取出。 - 遍历当前上下文中所有匹配节点,对每个匹配的候选节点执行展平。
- 类型检查:如果候选节点不是序列(
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错误。因此在使用前应确认目标节点是数组,例如先通过type或tag操作符判断类型,或确保查询路径一定指向数组字段。
深度参数的选取
- 需要完全拍平多层嵌套时,直接用
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 中区分flatten与flatten(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),仅供参考