diff --git a/pkg/yqlib/doc/operators/filter.md b/pkg/yqlib/doc/operators/filter.md new file mode 100644 index 00000000..d35269aa --- /dev/null +++ b/pkg/yqlib/doc/operators/filter.md @@ -0,0 +1,18 @@ + +## Filter array +Given a sample.yml file of: +```yaml +- 1 +- 2 +- 3 +``` +then +```bash +yq 'filter(. < 3)' sample.yml +``` +will output +```yaml +- 1 +- 2 +``` + diff --git a/pkg/yqlib/lexer_participle.go b/pkg/yqlib/lexer_participle.go index 33756fef..e6d4d58f 100644 --- a/pkg/yqlib/lexer_participle.go +++ b/pkg/yqlib/lexer_participle.go @@ -37,6 +37,7 @@ var participleYqRules = []*participleYqRule{ {"MapValues", `map_?values`, opToken(mapValuesOpType), 0}, simpleOp("map", mapOpType), + simpleOp("filter", filterOpType), simpleOp("pick", pickOpType), {"FlattenWithDepth", `flatten\([0-9]+\)`, flattenWithDepth(), 0}, diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 144fed9f..a24d18f2 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -84,6 +84,7 @@ var expressionOpType = &operationType{Type: "EXP", NumArgs: 0, Precedence: 50, H var collectOpType = &operationType{Type: "COLLECT", NumArgs: 1, Precedence: 50, Handler: collectOperator} var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: mapOperator} +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 evalOpType = &operationType{Type: "EVAL", NumArgs: 1, Precedence: 50, Handler: evalOperator} diff --git a/pkg/yqlib/operator_filter.go b/pkg/yqlib/operator_filter.go new file mode 100644 index 00000000..451b0beb --- /dev/null +++ b/pkg/yqlib/operator_filter.go @@ -0,0 +1,33 @@ +package yqlib + +import ( + "container/list" +) + +func filterOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + log.Debugf("-- filterOperation") + var results = list.New() + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + children := context.SingleChildContext(candidate) + splatted, err := splat(children, traversePreferences{}) + if err != nil { + return Context{}, err + } + filtered, err := selectOperator(d, splatted, expressionNode) + if err != nil { + return Context{}, err + } + + selfExpression := &ExpressionNode{Operation: &Operation{OperationType: selfReferenceOpType}} + collected, err := collectTogether(d, filtered, selfExpression) + if err != nil { + return Context{}, err + } + collected.Node.Style = unwrapDoc(candidate.Node).Style + results.PushBack(collected) + } + return context.ChildContext(results), nil +} + diff --git a/pkg/yqlib/operator_filter_test.go b/pkg/yqlib/operator_filter_test.go new file mode 100644 index 00000000..05a68161 --- /dev/null +++ b/pkg/yqlib/operator_filter_test.go @@ -0,0 +1,49 @@ +package yqlib + +import ( + "testing" +) + +var filterOperatorScenarios = []expressionScenario{ + { + description: "Filter array", + document: `[1,2,3]`, + expression: `filter(. < 3)`, + expected: []string{ + "D0, P[], (!!seq)::[1, 2]\n", + }, + }, + { + skipDoc: true, + document: `[1,2,3]`, + expression: `filter(. > 1)`, + expected: []string{ + "D0, P[], (!!seq)::[2, 3]\n", + }, + }, + { + skipDoc: true, + description: "Filter array to empty", + document: `[1,2,3]`, + expression: `filter(. > 4)`, + expected: []string{ + "D0, P[], (!!seq)::[]\n", + }, + }, + { + skipDoc: true, + description: "Filter empty array", + document: `[]`, + expression: `filter(. > 1)`, + expected: []string{ + "D0, P[], (!!seq)::[]\n", + }, + }, +} + +func TestFilterOperatorScenarios(t *testing.T) { + for _, tt := range filterOperatorScenarios { + testScenario(t, &tt) + } + documentOperatorScenarios(t, "filter", filterOperatorScenarios) +}