diff --git a/examples/data1.yaml b/examples/data1.yaml index 5be86e055a..26c3161c92 100644 --- a/examples/data1.yaml +++ b/examples/data1.yaml @@ -1,11 +1,3 @@ -# block comments come through -person: # neither do comments on maps - name: Mike Wazowski # comments on values appear - pets: - - cat # comments on array values appear - - dog # comments on array values appear - - things: - - frog - food: [pizza] # comments on arrays do not -emptyArray: [] -emptyMap: [] \ No newline at end of file +Foo: 3 +apple: 1 +bar: 2 \ No newline at end of file diff --git a/go.mod b/go.mod index b46d85e999..6733b5f7c8 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,4 @@ require ( golang.org/x/sys v0.28.0 // indirect ) -go 1.21.0 - -toolchain go1.22.5 +go 1.23.0 diff --git a/pkg/yqlib/candidate_node.go b/pkg/yqlib/candidate_node.go index 4762ea67d2..093d291df0 100644 --- a/pkg/yqlib/candidate_node.go +++ b/pkg/yqlib/candidate_node.go @@ -198,6 +198,29 @@ func (n *CandidateNode) SetParent(parent *CandidateNode) { n.Parent = parent } +type ValueVisitor func(*CandidateNode) error + +func (n *CandidateNode) VisitValues(visitor ValueVisitor) error { + if n.Kind == MappingNode { + for i := 1; i < len(n.Content); i = i + 2 { + if err := visitor(n.Content[i]); err != nil { + return err + } + } + } else if n.Kind == SequenceNode { + for i := 0; i < len(n.Content); i = i + 1 { + if err := visitor(n.Content[i]); err != nil { + return err + } + } + } + return nil +} + +func (n *CandidateNode) CanVisitValues() bool { + return n.Kind == MappingNode || n.Kind == SequenceNode +} + func (n *CandidateNode) AddKeyValueChild(rawKey *CandidateNode, rawValue *CandidateNode) (*CandidateNode, *CandidateNode) { key := rawKey.Copy() key.SetParent(n) diff --git a/pkg/yqlib/doc/operators/headers/sort-keys.md b/pkg/yqlib/doc/operators/headers/sort-keys.md index 92b83fe4d9..db94c9af5d 100644 --- a/pkg/yqlib/doc/operators/headers/sort-keys.md +++ b/pkg/yqlib/doc/operators/headers/sort-keys.md @@ -12,5 +12,5 @@ diff file1.yml file2.yml Note that `yq` does not yet consider anchors when sorting by keys - this may result in invalid yaml documents if you are using merge anchors. -For more advanced sorting, using `to_entries` to convert the map to an array, then sort/process the array as you like (e.g. using `sort_by`) and convert back to a map using `from_entries`. -See [here](https://mikefarah.gitbook.io/yq/operators/entries#custom-sort-map-keys) for an example. +For more advanced sorting, you can use the [sort_by](https://mikefarah.gitbook.io/yq/operators/sort) function on a map, and give it a custom function like `sort_by(key | downcase)`. + diff --git a/pkg/yqlib/doc/operators/sort-keys.md b/pkg/yqlib/doc/operators/sort-keys.md index fb8e86acaf..e0055b5380 100644 --- a/pkg/yqlib/doc/operators/sort-keys.md +++ b/pkg/yqlib/doc/operators/sort-keys.md @@ -12,8 +12,8 @@ diff file1.yml file2.yml Note that `yq` does not yet consider anchors when sorting by keys - this may result in invalid yaml documents if you are using merge anchors. -For more advanced sorting, using `to_entries` to convert the map to an array, then sort/process the array as you like (e.g. using `sort_by`) and convert back to a map using `from_entries`. -See [here](https://mikefarah.gitbook.io/yq/operators/entries#custom-sort-map-keys) for an example. +For more advanced sorting, you can use the [sort_by](https://mikefarah.gitbook.io/yq/operators/sort) function on a map, and give it a custom function like `sort_by(key | downcase)`. + ## Sort keys of map Given a sample.yml file of: diff --git a/pkg/yqlib/doc/operators/sort.md b/pkg/yqlib/doc/operators/sort.md index 72a1730514..17ae237d0e 100644 --- a/pkg/yqlib/doc/operators/sort.md +++ b/pkg/yqlib/doc/operators/sort.md @@ -109,6 +109,46 @@ cool: - c: banana ``` +## Sort a map +Sorting a map, by default, will sort by the values + +Given a sample.yml file of: +```yaml +y: b +z: a +x: c +``` +then +```bash +yq 'sort' sample.yml +``` +will output +```yaml +z: a +y: b +x: c +``` + +## Sort a map by keys +Use sort_by to sort a map using a custom function + +Given a sample.yml file of: +```yaml +Y: b +z: a +x: c +``` +then +```bash +yq 'sort_by(key | downcase)' sample.yml +``` +will output +```yaml +x: c +Y: b +z: a +``` + ## Sort is stable Note the order of the elements in unchanged when equal in sorting. diff --git a/pkg/yqlib/operator_sort.go b/pkg/yqlib/operator_sort.go index 942ef46a8c..c76b98469b 100644 --- a/pkg/yqlib/operator_sort.go +++ b/pkg/yqlib/operator_sort.go @@ -24,30 +24,40 @@ func sortByOperator(d *dataTreeNavigator, context Context, expressionNode *Expre for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) - if candidate.Kind != SequenceNode { - return context, fmt.Errorf("node at path [%v] is not an array (it's a %v)", candidate.GetNicePath(), candidate.Tag) - } - - sortableArray := make(sortableNodeArray, len(candidate.Content)) - - for i, originalNode := range candidate.Content { - - compareContext, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(originalNode), expressionNode.RHS) - if err != nil { - return Context{}, err + var sortableArray sortableNodeArray + + if candidate.CanVisitValues() { + sortableArray = make(sortableNodeArray, 0) + visitor := func(valueNode *CandidateNode) error { + compareContext, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(valueNode), expressionNode.RHS) + if err != nil { + return err + } + sortableNode := sortableNode{Node: valueNode, CompareContext: compareContext, dateTimeLayout: context.GetDateTimeLayout()} + sortableArray = append(sortableArray, sortableNode) + return nil } - - sortableArray[i] = sortableNode{Node: originalNode, CompareContext: compareContext, dateTimeLayout: context.GetDateTimeLayout()} - + if err := candidate.VisitValues(visitor); err != nil { + return context, err + } + } else { + return context, fmt.Errorf("node at path [%v] is not an array or map (it's a %v)", candidate.GetNicePath(), candidate.Tag) } sort.Stable(sortableArray) - sortedList := candidate.CreateReplacementWithComments(SequenceNode, "!!seq", candidate.Style) - - for _, sortedNode := range sortableArray { - sortedList.AddChild(sortedNode.Node) + sortedList := candidate.CopyWithoutContent() + if candidate.Kind == MappingNode { + for _, sortedNode := range sortableArray { + sortedList.AddKeyValueChild(sortedNode.Node.Key, sortedNode.Node) + } + } else if candidate.Kind == SequenceNode { + for _, sortedNode := range sortableArray { + sortedList.AddChild(sortedNode.Node) + } } + + // convert array of value nodes back to map results.PushBack(sortedList) } return context.ChildContext(results), nil diff --git a/pkg/yqlib/operator_sort_test.go b/pkg/yqlib/operator_sort_test.go index df1bbb0f8c..efb2a2d721 100644 --- a/pkg/yqlib/operator_sort_test.go +++ b/pkg/yqlib/operator_sort_test.go @@ -84,6 +84,24 @@ var sortByOperatorScenarios = []expressionScenario{ "D0, P[], (!!map)::cool: [{a: banana}, {b: banana}, {c: banana}]\n", }, }, + { + description: "Sort a map", + subdescription: "Sorting a map, by default this will sort by the values", + document: "y: b\nz: a\nx: c\n", + expression: `sort`, + expected: []string{ + "D0, P[], (!!map)::z: a\ny: b\nx: c\n", + }, + }, + { + description: "Sort a map by keys", + subdescription: "Use sort_by to sort a map using a custom function", + document: "Y: b\nz: a\nx: c\n", + expression: `sort_by(key | downcase)`, + expected: []string{ + "D0, P[], (!!map)::x: c\nY: b\nz: a\n", + }, + }, { description: "Sort is stable", subdescription: "Note the order of the elements in unchanged when equal in sorting.",