diff --git a/pkg/yqlib/doc/operators/omit.md b/pkg/yqlib/doc/operators/omit.md new file mode 100644 index 00000000..273b3956 --- /dev/null +++ b/pkg/yqlib/doc/operators/omit.md @@ -0,0 +1,41 @@ + +## Omit keys from map +Note that the order of the keys matches the omit order and non existent keys are skipped. + +Given a sample.yml file of: +```yaml +myMap: + cat: meow + dog: bark + thing: hamster + hamster: squeak +``` +then +```bash +yq '.myMap |= omit(["hamster", "cat", "goat"])' sample.yml +``` +will output +```yaml +myMap: + dog: bark + thing: hamster +``` + +## Omit indices from array +Note that the order of the indices matches the omit order and non existent indices are skipped. + +Given a sample.yml file of: +```yaml +- cat +- leopard +- lion +``` +then +```bash +yq 'omit([2, 0, 734, -5])' sample.yml +``` +will output +```yaml +- leopard +``` + diff --git a/pkg/yqlib/lexer_participle.go b/pkg/yqlib/lexer_participle.go index b7ccc5d2..2f65bddc 100644 --- a/pkg/yqlib/lexer_participle.go +++ b/pkg/yqlib/lexer_participle.go @@ -40,6 +40,7 @@ var participleYqRules = []*participleYqRule{ simpleOp("map", mapOpType), simpleOp("filter", filterOpType), simpleOp("pick", pickOpType), + simpleOp("omit", omitOpType), {"FlattenWithDepth", `flatten\([0-9]+\)`, flattenWithDepth(), 0}, {"Flatten", `flatten`, opTokenWithPrefs(flattenOpType, nil, flattenPreferences{depth: -1}), 0}, diff --git a/pkg/yqlib/operation.go b/pkg/yqlib/operation.go index ea29e10d..df89247c 100644 --- a/pkg/yqlib/operation.go +++ b/pkg/yqlib/operation.go @@ -90,6 +90,7 @@ var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: var filterOpType = &operationType{Type: "FILTER", NumArgs: 1, Precedence: 50, Handler: filterOperator} var errorOpType = &operationType{Type: "ERROR", NumArgs: 1, Precedence: 50, Handler: errorOperator} var pickOpType = &operationType{Type: "PICK", NumArgs: 1, Precedence: 50, Handler: pickOperator} +var omitOpType = &operationType{Type: "OMIT", NumArgs: 1, Precedence: 50, Handler: omitOperator} var evalOpType = &operationType{Type: "EVAL", NumArgs: 1, Precedence: 50, Handler: evalOperator} var mapValuesOpType = &operationType{Type: "MAP_VALUES", NumArgs: 1, Precedence: 50, Handler: mapValuesOperator} diff --git a/pkg/yqlib/operator_omit.go b/pkg/yqlib/operator_omit.go new file mode 100644 index 00000000..45ad7ce9 --- /dev/null +++ b/pkg/yqlib/operator_omit.go @@ -0,0 +1,73 @@ +package yqlib + +import ( + "container/list" + "strconv" +) + +func omitMap(original *CandidateNode, indices *CandidateNode) *CandidateNode { + filteredContent := make([]*CandidateNode, 0, max(0, len(original.Content)-len(indices.Content)*2)) + + for index := 0; index < len(original.Content); index += 2 { + pos := findInArray(indices, original.Content[index]) + if pos < 0 { + clonedKey := original.Content[index].Copy() + clonedValue := original.Content[index+1].Copy() + filteredContent = append(filteredContent, clonedKey, clonedValue) + } + } + result := original.CopyWithoutContent() + result.AddChildren(filteredContent) + return result +} + +func omitSequence(original *CandidateNode, indices *CandidateNode) *CandidateNode { + filteredContent := make([]*CandidateNode, 0, max(0, len(original.Content)-len(indices.Content))) + + for index := 0; index < len(original.Content); index++ { + pos := findInArray(indices, createScalarNode(index, strconv.Itoa(index))) + if pos < 0 { + filteredContent = append(filteredContent, original.Content[index].Copy()) + } + } + result := original.CopyWithoutContent() + result.AddChildren(filteredContent) + return result +} + +func omitOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + log.Debugf("Omit") + + contextIndicesToOmit, err := d.GetMatchingNodes(context, expressionNode.RHS) + + if err != nil { + return Context{}, err + } + indicesToOmit := &CandidateNode{} + if contextIndicesToOmit.MatchingNodes.Len() > 0 { + indicesToOmit = contextIndicesToOmit.MatchingNodes.Front().Value.(*CandidateNode) + } + if len(indicesToOmit.Content) == 0 { + log.Debugf("No omit indices specified") + return context, nil + } + var results = list.New() + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + node := el.Value.(*CandidateNode) + + var replacement *CandidateNode + + if node.Kind == MappingNode { + replacement = omitMap(node, indicesToOmit) + } else if node.Kind == SequenceNode { + replacement = omitSequence(node, indicesToOmit) + } else { + log.Debugf("Omit from type %v (%v) is noop", node.Tag, node.GetNicePath()) + return context, nil + } + replacement.LeadingContent = node.LeadingContent + results.PushBack(replacement) + } + return context.ChildContext(results), nil +} diff --git a/pkg/yqlib/operator_omit_test.go b/pkg/yqlib/operator_omit_test.go new file mode 100644 index 00000000..5f264b61 --- /dev/null +++ b/pkg/yqlib/operator_omit_test.go @@ -0,0 +1,60 @@ +package yqlib + +import ( + "testing" +) + +var omitOperatorScenarios = []expressionScenario{ + { + description: "Omit keys from map", + subdescription: "Note that the order of the keys matches the omit order and non existent keys are skipped.", + document: "myMap: {cat: meow, dog: bark, thing: hamster, hamster: squeak}\n", + expression: `.myMap |= omit(["hamster", "cat", "goat"])`, + expected: []string{ + "D0, P[], (!!map)::myMap: {dog: bark, thing: hamster}\n", + }, + }, + { + description: "Omit keys from map", + skipDoc: true, + document: "!things myMap: {cat: meow, dog: bark, thing: hamster, hamster: squeak}\n", + expression: `.myMap |= omit(["hamster", "cat", "goat"])`, + expected: []string{ + "D0, P[], (!!map)::!things myMap: {dog: bark, thing: hamster}\n", + }, + }, + { + description: "Omit keys from map with comments", + skipDoc: true, + document: "# abc\nmyMap: {cat: meow, dog: bark, thing: hamster, hamster: squeak}\n# xyz\n", + expression: `.myMap |= omit(["hamster", "cat", "goat"])`, + expected: []string{ + "D0, P[], (!!map)::# abc\nmyMap: {dog: bark, thing: hamster}\n# xyz\n", + }, + }, + { + description: "Omit indices from array", + subdescription: "Note that the order of the indices matches the omit order and non existent indices are skipped.", + document: `[cat, leopard, lion]`, + expression: `omit([2, 0, 734, -5])`, + expected: []string{ + "D0, P[], (!!seq)::[leopard]\n", + }, + }, + { + description: "Omit indices from array with comments", + skipDoc: true, + document: "# abc\n[cat, leopard, lion]\n# xyz", + expression: `omit([2, 0, 734, -5])`, + expected: []string{ + "D0, P[], (!!seq)::# abc\n[leopard]\n# xyz\n", + }, + }, +} + +func TestOmitOperatorScenarios(t *testing.T) { + for _, tt := range omitOperatorScenarios { + testScenario(t, &tt) + } + documentOperatorScenarios(t, "omit", omitOperatorScenarios) +}