From 8e14b3b393be191484916989df12f97f61bf0011 Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Fri, 14 May 2021 14:29:55 +1000 Subject: [PATCH] Added any and all operators --- pkg/yqlib/doc/Boolean Operators.md | 81 +++++++++++++++++++++- pkg/yqlib/doc/headers/Boolean Operators.md | 8 ++- pkg/yqlib/expression_tokeniser.go | 3 + pkg/yqlib/lib.go | 3 + pkg/yqlib/operator_booleans.go | 63 +++++++++++++++-- pkg/yqlib/operator_booleans_test.go | 56 +++++++++++++++ 6 files changed, 208 insertions(+), 6 deletions(-) diff --git a/pkg/yqlib/doc/Boolean Operators.md b/pkg/yqlib/doc/Boolean Operators.md index 7955937c..489474c7 100644 --- a/pkg/yqlib/doc/Boolean Operators.md +++ b/pkg/yqlib/doc/Boolean Operators.md @@ -1,4 +1,10 @@ -The `or` and `and` operators take two parameters and return a boolean result. `not` flips a boolean from true to false, or vice versa. These are most commonly used with the `select` operator to filter particular nodes. +The `or` and `and` operators take two parameters and return a boolean result. + +`not` flips a boolean from true to false, or vice versa. + +`any` will return `true` if there are any `true` values in a array sequence, and `all` will return true if _all_ elements in an array are true. + +These are most commonly used with the `select` operator to filter particular nodes. ## OR example Running ```bash @@ -41,6 +47,79 @@ will output b: fly ``` +## ANY returns true if any boolean in a given array is true +Given a sample.yml file of: +```yaml +- false +- true +``` +then +```bash +yq eval 'any' sample.yml +``` +will output +```yaml +true +``` + +## ANY returns true if any boolean in a given array is true +Given a sample.yml file of: +```yaml +- false +- true +``` +then +```bash +yq eval 'any' sample.yml +``` +will output +```yaml +true +``` + +## ANY returns false for an empty array +Given a sample.yml file of: +```yaml +[] +``` +then +```bash +yq eval 'any' sample.yml +``` +will output +```yaml +false +``` + +## ALL returns true if all booleans in a given array are true +Given a sample.yml file of: +```yaml +- true +- true +``` +then +```bash +yq eval 'all' sample.yml +``` +will output +```yaml +true +``` + +## ANY returns true for an empty array +Given a sample.yml file of: +```yaml +[] +``` +then +```bash +yq eval 'all' sample.yml +``` +will output +```yaml +true +``` + ## Not true is false Running ```bash diff --git a/pkg/yqlib/doc/headers/Boolean Operators.md b/pkg/yqlib/doc/headers/Boolean Operators.md index 69c46bda..6cdb9b99 100644 --- a/pkg/yqlib/doc/headers/Boolean Operators.md +++ b/pkg/yqlib/doc/headers/Boolean Operators.md @@ -1 +1,7 @@ -The `or` and `and` operators take two parameters and return a boolean result. `not` flips a boolean from true to false, or vice versa. These are most commonly used with the `select` operator to filter particular nodes. \ No newline at end of file +The `or` and `and` operators take two parameters and return a boolean result. + +`not` flips a boolean from true to false, or vice versa. + +`any` will return `true` if there are any `true` values in a array sequence, and `all` will return true if _all_ elements in an array are true. + +These are most commonly used with the `select` operator to filter particular nodes. \ No newline at end of file diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index 9f00ed21..e9cb514f 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -276,6 +276,9 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`join`), opToken(joinStringOpType)) lexer.Add([]byte(`sub`), opToken(subStringOpType)) + lexer.Add([]byte(`any`), opToken(anyOpType)) + lexer.Add([]byte(`all`), opToken(allOpType)) + lexer.Add([]byte(`split`), opToken(splitStringOpType)) lexer.Add([]byte(`keys`), opToken(keysOpType)) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index a9ece184..5bf75a6f 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -61,6 +61,9 @@ var shortPipeOpType = &operationType{Type: "SHORT_PIPE", NumArgs: 2, Precedence: var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator} var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: collectOperator} +var anyOpType = &operationType{Type: "ANY", NumArgs: 0, Precedence: 50, Handler: anyOperator} +var allOpType = &operationType{Type: "ALL", NumArgs: 0, Precedence: 50, Handler: allOperator} + var toEntriesOpType = &operationType{Type: "TO_ENTRIES", NumArgs: 0, Precedence: 50, Handler: toEntriesOperator} var fromEntriesOpType = &operationType{Type: "FROM_ENTRIES", NumArgs: 0, Precedence: 50, Handler: fromEntriesOperator} var withEntriesOpType = &operationType{Type: "WITH_ENTRIES", NumArgs: 1, Precedence: 50, Handler: withEntriesOperator} diff --git a/pkg/yqlib/operator_booleans.go b/pkg/yqlib/operator_booleans.go index f813b9b5..4affde91 100644 --- a/pkg/yqlib/operator_booleans.go +++ b/pkg/yqlib/operator_booleans.go @@ -2,14 +2,12 @@ package yqlib import ( "container/list" - + "fmt" yaml "gopkg.in/yaml.v3" ) -func isTruthy(c *CandidateNode) (bool, error) { - node := unwrapDoc(c.Node) +func isTruthyNode(node * yaml.Node) (bool, error) { value := true - if node.Tag == "!!null" { return false, nil } @@ -23,6 +21,11 @@ func isTruthy(c *CandidateNode) (bool, error) { return value, nil } +func isTruthy(c *CandidateNode) (bool, error) { + node := unwrapDoc(c.Node) + return isTruthyNode(node) +} + type boolOp func(bool, bool) bool func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { @@ -61,6 +64,58 @@ func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *C } } +func findBoolean(wantBool bool, sequenceNode * yaml.Node) (bool, error) { + for _, node := range sequenceNode.Content { + + truthy, err := isTruthyNode(node) + if err != nil { + return false, err + } + if truthy == wantBool { + return true, nil + } + } + return false, nil +} + +func allOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + var results = list.New() + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + candidateNode := unwrapDoc(candidate.Node) + if candidateNode.Kind != yaml.SequenceNode { + return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag) + } + booleanResult, err := findBoolean(false, candidateNode) + if err != nil { + return Context{}, err + } + result := createBooleanCandidate(candidate, !booleanResult) + results.PushBack(result) + } + return context.ChildContext(results), nil +} + +func anyOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + var results = list.New() + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + candidateNode := unwrapDoc(candidate.Node) + if candidateNode.Kind != yaml.SequenceNode { + return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag) + } + booleanResult, err := findBoolean(true, candidateNode) + if err != nil { + return Context{}, err + } + result := createBooleanCandidate(candidate, booleanResult) + results.PushBack(result) + } + return context.ChildContext(results), nil +} + func orOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { log.Debugf("-- orOp") return crossFunction(d, context, expressionNode, performBoolOp( diff --git a/pkg/yqlib/operator_booleans_test.go b/pkg/yqlib/operator_booleans_test.go index b7554093..72b318cc 100644 --- a/pkg/yqlib/operator_booleans_test.go +++ b/pkg/yqlib/operator_booleans_test.go @@ -43,6 +43,62 @@ var booleanOperatorScenarios = []expressionScenario{ "D0, P[], (!!seq)::- {a: bird, b: dog}\n- {a: cat, b: fly}\n", }, }, + { + description: "ANY returns true if any boolean in a given array is true", + document: `[false, true]`, + expression: "any", + expected: []string{ + "D0, P[], (!!bool)::true\n", + }, + }, + { + description: "ANY returns true if any boolean in a given array is true", + document: `[false, true]`, + expression: "any", + expected: []string{ + "D0, P[], (!!bool)::true\n", + }, + }, + { + description: "ANY returns false for an empty array", + document: `[]`, + expression: "any", + expected: []string{ + "D0, P[], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: `[false, false]`, + expression: "any", + expected: []string{ + "D0, P[], (!!bool)::false\n", + }, + }, + { + description: "ALL returns true if all booleans in a given array are true", + document: `[true, true]`, + expression: "all", + expected: []string{ + "D0, P[], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + document: `[false, true]`, + expression: "all", + expected: []string{ + "D0, P[], (!!bool)::false\n", + }, + }, + { + description: "ANY returns true for an empty array", + document: `[]`, + expression: "all", + expected: []string{ + "D0, P[], (!!bool)::true\n", + }, + }, { skipDoc: true, expression: `false or false`,