yq sort / sort_by 排序算子完全指南:从字符串、数字、日期到稳定排序与源码实现
yq sort / sort_by 排序算子完全指南从字符串、数字、日期到稳定排序与源码实现【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq本篇以 yq 操作符文档 sort.md 为主体系统讲解sort与sort_by的用法按字符串/数字/日期字段排序、多字段排序、降序、就地排序、Map 排序、稳定性与 null 排序规则并结合 yq 排序算子的源码实现与测试用例说明底层比较逻辑帮助你在处理 YAML、JSON、XML、CSV、TOML、HCL 等格式数据时完成各种排序场景。sort 与 sort_by 的基本语义yq 提供了两个基础排序入口sort对当前节点原样排序即以节点自身值作为比较依据sort_by(exp)按指定表达式exp计算出的值排序例如按子字段.a排序。两条重要的使用约定源自官方文档的明确说明降序排序yq 没有内置降序参数惯例做法是排序后再通过管道接上reverse算子参见 reverse.md只排序标量字段yq 在比较阶段只支持标量scalar值比较表达式最终必须落到字符串、数字、布尔、null、时间戳这类标量上。从源码结构看sort并非独立实现而是sort_by的语法糖operator_sort.go 中sortOperator会把自身引用self作为右操作数挂到sort_by上再执行因此两者的行为、稳定性、错误处理完全一致。按字符串字段排序给定sample.yml- a: banana - a: cat - a: apple执行yq sort_by(.a) sample.yml输出- a: apple - a: banana - a: cat排序作用在当前匹配节点的数组或 Map上sort_by(.a)表示对每个元素取子字段a的值作为比较键。这个场景在 operator_sort_test.go 中有对应测试且测试支持sort_by(.a)[]这种把排序结果再逐元素展开的写法。一个值得注意的边界当数组中混有null元素时sort_by(.a)不会报错null 元素按规则排到最前见 operator_sort_test.go 中 Sort by with null 场景。按多个字段排序当单一字段出现相同值时sort_by接受多个比较表达式实现类似 SQL 的ORDER BY语义。给定sample.yml- a: dog - a: cat b: banana - a: cat b: apple执行yq sort_by(.a, .b) sample.yml输出- a: cat b: apple - a: cat b: banana - a: dog测试用例进一步验证了两个细节见 operator_sort_test.go若第二关键字段缺失如某元素没有b缺失侧按更短处理排在有值侧之前[{a: dog, b: good}, {a: cat, c: things}, {a: cat, b: apple}]排序后c: things那个元素排在b: apple之前多个字段同样支持小数比较0.001 0.01 0.1按数值大小而非字符串顺序排列。这一行为对应 operator_sort.go 中Less方法的实现它逐个比较左右两侧比较上下文的匹配节点当一侧比较键用尽而另一侧还有剩余时返回lhsContext.MatchingNodes.Len() rhsContext.MatchingNodes.Len()即键更短的一侧排前。降序排序sort_by 接 reverseyq 中没有sort_desc之类的算子官方推荐的降序方式是排序后接reverse给定sample.yml- a: banana - a: cat - a: apple执行yq sort_by(.a) | reverse sample.yml输出- a: cat - a: banana - a: apple由于排序是稳定排序下文详述先按升序排、再整体反转得到的就是严格降序相同键值的元素之间相对次序会随之反转这是该组合方式的固有行为需要留意。就地排序数组使用|更新赋值sort/sort_by的语义是把数组灌进去、排出一个排好序的数组它本身不会改写源节点。要把排序结果写回原字段需要用更新赋值|给定sample.ymlcool: - a: banana - a: cat - a: apple执行yq .cool | sort_by(.a) sample.yml输出cool: - a: apple - a: banana - a: cat|等价于.cool (.cool | sort_by(.a))。这一模式在官方 recipes 文档中也被反复使用例如 recipes_test.go 中.myArray | sort_by(.numBuckets)的注释明确解释了该等价关系。用复杂表达式排序按键名排序对象数组sort_by的实参是任意表达式而不只是字段路径。例如对象数组按每个对象自身的第一个键名排序给定sample.ymlcool: - b: banana - a: banana - c: banana执行yq .cool | sort_by(keys | .[0]) sample.yml输出cool: - a: banana - b: banana - c: bananakeys | .[0]对每个元素求键名数组再取首元素比较键因此是a、b、c。由此可以组合出更多高级用法例如sort_by(length)按字符串长度、sort_by(.date | to_number)等——只要表达式最终产出标量即可。对 Map 排序默认按值可自定义按键sort同样能作用于 Map。直接对 Map 执行sort时值是各键对应的 value 节点因此默认按 value 排序键随值一起移动给定sample.ymly: b z: a x: c执行yq sort sample.yml输出z: a y: b x: c若希望按键排序可以给sort_by一个自定义表达式。注意 yq 中key是当前键名的内置变量与.当前值区分配合字符串函数可定制排序规则。例如忽略大小写按键排序给定sample.ymlY: b z: a x: c执行yq sort_by(key | downcase) sample.yml输出x: c Y: b z: a这里x Y z是因为按downcase后的键比较即x、y、z。补充说明如果只是想按键的字符串字典序排序不做自定义逻辑yq 还提供了专门的sort_keys算子它对数组和标量不做任何事因此可以安全地用sort_keys(..)递归应用到所有 Map常用于对两个文档做 diff 前的规范化参见 sort-keys.md而sort_by(key | ...)则提供了更灵活的自定义排序能力两者互补。排序是稳定排序yq 的排序是稳定的当比较键相等时元素的原始相对顺序保持不变。给定sample.yml- a: banana b: 1 - a: banana b: 2 - a: banana b: 3 - a: banana b: 4执行yq sort_by(.a) sample.yml输出与输入顺序完全一致b: 1到b: 4保持原位。这一特性有明确的源码依据operator_sort.go 中调用的是 Go 标准库的sort.Stable(sortableArray)而非sort.Sort因此等键保序是算法层面的保证而不是碰巧。测试用例 operator_sort_test.goSort is stable和布尔值场景false元素之间保持b: 1在b: 3前都在持续验证该行为。稳定性意味着多字段排序、以及排序 后续 filter组合可以产生确定性的、可复现的结果。按数字字段排序数值比较而非字符串比较给定sample.yml- a: 10 - a: 100 - a: 1执行yq sort_by(.a) sample.yml输出- a: 1 - a: 10 - a: 100如果按字符串字典序比较1 10 100恰好结果相同但9与10的字典序是反的——yq 对数值类型走的是真正的数值比较。源码 operator_sort.go 显示两侧都是!!int时用parseInt64做 int64 比较因此超大整数如 ±5×10^18也能正确排序而不发生减法溢出对应测试 Sort large integers (no int64 subtraction overflow)operator_sort_test.goint 与 float 混合时统一ParseFloat为 float64 比较测试中1.001 1.01 1.1的顺序即按数值大小得出。按自定义日期格式排序with_dtfYAML 中未带时间的日期如12-Jun-2011默认按字符串处理直接sort_by会按字典序排错。yq 提供with_dtf(layout; expr)算子在限定表达式expr的作用域内把日期时间解析的布局临时切换为指定格式Go time 的 layout 记法。给定sample.yml- a: 12-Jun-2011 - a: 23-Dec-2010 - a: 10-Aug-2011执行yq with_dtf(02-Jan-2006; sort_by(.a)) sample.yml输出- a: 23-Dec-2010 - a: 12-Jun-2011 - a: 10-Aug-2011底层实现见 operator_sort.go比较时若两侧都是!!timestamp或布局非 RFC3339 且两侧字符串都能按该布局成功parseDateTime就按时间先后比较解析失败时打印警告并回退为字符串比较log.Warningf(Could not parse time ... sorting by string instead)。因此使用with_dtf时布局必须与数据实际格式严格一致否则会出现静默退化为字典序的排序结果。null 与布尔值的排序位次对混合类型数组直接执行sort时比较逻辑遵循固定的类型优先次序。给定sample.yml- 8 - 3 - null - 6 - true - false - cat执行yq sort sample.yml输出- null - false - true - 3 - 6 - 8 - cat即排序位次为null 最先然后是布尔false 在 true 前再是数字字符串殿后。该规则与 operator_sort.go 中compare方法的分支完全对应if lhsTag !!null rhsTag ! !!null { return -1 } else if lhsTag ! !!null rhsTag !!null { return 1 } else if lhsTag !!bool rhsTag ! !!bool { return -1 } else if lhsTag ! !!bool rhsTag !!bool { return 1 } else if lhsTag !!bool rhsTag !!bool { // false 在 true 前 }两侧类型不同且都不属于上述类别时最终落入strings.Compare(lhs.Value, rhs.Value)的字符串兜底比较。理解这条类型优先链有助于解释为什么混排数组中数字总是排在字符串前面、null 永远沉底升序时置顶。源码纵览sort_by 的执行流程综合 operator_sort.go 的实现sort_by的完整执行流程是对每个匹配的候选节点candidate要求其CanVisitValues()——即必须是数组或 Map否则报错node at path [...] is not an array or mapoperator_sort.go通过VisitValues遍历每个值节点对每个值在单元素只读上下文中求值expressionNode.RHS即sort_by的实参得到该元素的比较上下文CompareContext与值节点一起封装为sortableNode用sort.Stable排序sortableNodeArray其Less逐位比较左右两个比较上下文全部相等时以比较键个数少者在前裁决多字段排序的关键字回退规则排序完成后用CopyWithoutContent()复制原容器Map 按新顺序AddKeyValueChild重新挂键值对数组Sequence按新顺序AddChild重新挂载保持原节点的样式与结构信息。算子注册方面sort在 operation.go 中定义为NumArgs: 0、Precedence: 52、CheckForPostTraverse: true的操作类型词法层在 lexer_participle.go 以simpleOp(sort, sortOpType)登记sort_by则作为带右操作数的形式走同一处理器。适用前提与限制小结sort/sort_by的作用节点必须是数组或 Map对纯标量使用会直接报错not an array or map比较表达式必须产出标量官方文档明确at this stage, yq only sorts scalar fields复杂嵌套值无法直接参与比较降序需要| reverse组合实现自定义日期排序依赖with_dtf且格式串错误时只降级为字符串比较并输出警告需自行核对稳定性由sort.Stable保证等键元素保序所有示例均可通过 operator_sort_test.go 中的场景列表复现验证测试与文档由同一份场景数据驱动documentOperatorScenarios因此文档示例与代码行为始终对齐。【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考