diff --git a/pkg/yqlib/doc/Boolean Operators.md b/pkg/yqlib/doc/Boolean Operators.md index 489474c7..fccf1c1c 100644 --- a/pkg/yqlib/doc/Boolean Operators.md +++ b/pkg/yqlib/doc/Boolean Operators.md @@ -4,8 +4,11 @@ The `or` and `and` operators take two parameters and return a boolean result. `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. +`any_c(condition)` and `all_c(condition)` are like `any` and `all` but they take a condition expression that is used against each element to determine if it's `true`. Note: in `jq` you can simply pass a condition to `any` or `all` and it simply works - `yq` isn't that clever..yet + These are most commonly used with the `select` operator to filter particular nodes. -## OR example + +## `or` example Running ```bash yq eval --null-input 'true or false' @@ -15,7 +18,7 @@ will output true ``` -## AND example +## `and` example Running ```bash yq eval --null-input 'true and false' @@ -47,7 +50,7 @@ will output b: fly ``` -## ANY returns true if any boolean in a given array is true +## `any` returns true if any boolean in a given array is true Given a sample.yml file of: ```yaml - false @@ -62,22 +65,7 @@ will output 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 +## `any` returns false for an empty array Given a sample.yml file of: ```yaml [] @@ -91,7 +79,27 @@ will output false ``` -## ALL returns true if all booleans in a given array are true +## `any_c` returns true if any element in the array is true for the given condition. +Given a sample.yml file of: +```yaml +a: + - rad + - awesome +b: + - meh + - whatever +``` +then +```bash +yq eval '.[] |= any_c(. == "awesome")' sample.yml +``` +will output +```yaml +a: true +b: false +``` + +## `all` returns true if all booleans in a given array are true Given a sample.yml file of: ```yaml - true @@ -106,7 +114,7 @@ will output true ``` -## ANY returns true for an empty array +## `all` returns true for an empty array Given a sample.yml file of: ```yaml [] @@ -120,6 +128,26 @@ will output true ``` +## `all_c` returns true if all elements in the array are true for the given condition. +Given a sample.yml file of: +```yaml +a: + - rad + - awesome +b: + - meh + - 12 +``` +then +```bash +yq eval '.[] |= all_c(tag == "!!str")' sample.yml +``` +will output +```yaml +a: true +b: false +``` + ## 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 6cdb9b99..4e4a8d54 100644 --- a/pkg/yqlib/doc/headers/Boolean Operators.md +++ b/pkg/yqlib/doc/headers/Boolean Operators.md @@ -4,4 +4,6 @@ The `or` and `and` operators take two parameters and return a boolean result. `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 +`any_c(condition)` and `all_c(condition)` are like `any` and `all` but they take a condition expression that is used against each element to determine if it's `true`. Note: in `jq` you can simply pass a condition to `any` or `all` and it simply works - `yq` isn't that clever..yet + +These are most commonly used with the `select` operator to filter particular nodes. diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index e9cb514f..888b882d 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -277,7 +277,9 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`sub`), opToken(subStringOpType)) lexer.Add([]byte(`any`), opToken(anyOpType)) + lexer.Add([]byte(`any_c`), opToken(anyConditionOpType)) lexer.Add([]byte(`all`), opToken(allOpType)) + lexer.Add([]byte(`all_c`), opToken(allConditionOpType)) 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 5bf75a6f..44dd3b21 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -63,6 +63,8 @@ var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, var anyOpType = &operationType{Type: "ANY", NumArgs: 0, Precedence: 50, Handler: anyOperator} var allOpType = &operationType{Type: "ALL", NumArgs: 0, Precedence: 50, Handler: allOperator} +var anyConditionOpType = &operationType{Type: "ANY_CONDITION", NumArgs: 1, Precedence: 50, Handler: anyOperator} +var allConditionOpType = &operationType{Type: "ALL_CONDITION", NumArgs: 1, 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} diff --git a/pkg/yqlib/operator_booleans.go b/pkg/yqlib/operator_booleans.go index 4affde91..02c3e0d7 100644 --- a/pkg/yqlib/operator_booleans.go +++ b/pkg/yqlib/operator_booleans.go @@ -3,10 +3,11 @@ package yqlib import ( "container/list" "fmt" + yaml "gopkg.in/yaml.v3" ) -func isTruthyNode(node * yaml.Node) (bool, error) { +func isTruthyNode(node *yaml.Node) (bool, error) { value := true if node.Tag == "!!null" { return false, nil @@ -64,9 +65,24 @@ func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *C } } -func findBoolean(wantBool bool, sequenceNode * yaml.Node) (bool, error) { +func findBoolean(wantBool bool, d *dataTreeNavigator, context Context, expressionNode *ExpressionNode, sequenceNode *yaml.Node) (bool, error) { for _, node := range sequenceNode.Content { + if expressionNode != nil { + //need to evaluate the expression against the node + candidate := &CandidateNode{Node: node} + rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode) + if err != nil { + return false, err + } + if rhs.MatchingNodes.Len() > 0 { + node = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node + } else { + // no results found, ignore this entry + continue + } + } + truthy, err := isTruthyNode(node) if err != nil { return false, err @@ -87,7 +103,7 @@ func allOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi if candidateNode.Kind != yaml.SequenceNode { return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag) } - booleanResult, err := findBoolean(false, candidateNode) + booleanResult, err := findBoolean(false, d, context, expressionNode.Rhs, candidateNode) if err != nil { return Context{}, err } @@ -106,7 +122,7 @@ func anyOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi if candidateNode.Kind != yaml.SequenceNode { return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag) } - booleanResult, err := findBoolean(true, candidateNode) + booleanResult, err := findBoolean(true, d, context, expressionNode.Rhs, candidateNode) if err != nil { return Context{}, err } diff --git a/pkg/yqlib/operator_booleans_test.go b/pkg/yqlib/operator_booleans_test.go index 72b318cc..c505ab81 100644 --- a/pkg/yqlib/operator_booleans_test.go +++ b/pkg/yqlib/operator_booleans_test.go @@ -6,7 +6,7 @@ import ( var booleanOperatorScenarios = []expressionScenario{ { - description: "OR example", + description: "`or` example", expression: `true or false`, expected: []string{ "D0, P[], (!!bool)::true\n", @@ -29,7 +29,7 @@ var booleanOperatorScenarios = []expressionScenario{ }, }, { - description: "AND example", + description: "`and` example", expression: `true and false`, expected: []string{ "D0, P[], (!!bool)::false\n", @@ -44,61 +44,69 @@ var booleanOperatorScenarios = []expressionScenario{ }, }, { - description: "ANY returns true if any boolean in a given array is true", - document: `[false, true]`, - expression: "any", + 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", + description: "`any` returns false for an empty array", + document: `[]`, + expression: "any", expected: []string{ - "D0, P[], (!!bool)::true\n", + "D0, P[], (!!bool)::false\n", }, }, { - description: "ANY returns false for an empty array", - document: `[]`, + description: "`any_c` returns true if any element in the array is true for the given condition.", + document: "a: [rad, awesome]\nb: [meh, whatever]", + expression: `.[] |= any_c(. == "awesome")`, + expected: []string{ + "D0, P[], (doc)::a: true\nb: false\n", + }, + }, + { + skipDoc: true, + document: `[false, false]`, 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", + 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]`, + 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", + description: "`all` returns true for an empty array", + document: `[]`, + expression: "all", expected: []string{ "D0, P[], (!!bool)::true\n", }, }, + { + description: "`all_c` returns true if all elements in the array are true for the given condition.", + document: "a: [rad, awesome]\nb: [meh, 12]", + expression: `.[] |= all_c(tag == "!!str")`, + expected: []string{ + "D0, P[], (doc)::a: true\nb: false\n", + }, + }, { skipDoc: true, expression: `false or false`, diff --git a/pkg/yqlib/operator_entries_test.go b/pkg/yqlib/operator_entries_test.go index 0c0e3733..f195c4ba 100644 --- a/pkg/yqlib/operator_entries_test.go +++ b/pkg/yqlib/operator_entries_test.go @@ -25,8 +25,7 @@ var entriesOperatorScenarios = []expressionScenario{ description: "to_entries null", document: `null`, expression: `to_entries`, - expected: []string{ - }, + expected: []string{}, }, { description: "from_entries map", diff --git a/pkg/yqlib/operator_unique.go b/pkg/yqlib/operator_unique.go index 5de638a2..92e1a647 100644 --- a/pkg/yqlib/operator_unique.go +++ b/pkg/yqlib/operator_unique.go @@ -1,10 +1,11 @@ package yqlib import ( - "github.com/elliotchance/orderedmap" "container/list" - yaml "gopkg.in/yaml.v3" "fmt" + + "github.com/elliotchance/orderedmap" + yaml "gopkg.in/yaml.v3" ) func unique(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { @@ -19,7 +20,6 @@ func uniqueBy(d *dataTreeNavigator, context Context, expressionNode *ExpressionN log.Debugf("-- uniqueBy Operator") var results = list.New() - for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) candidateNode := unwrapDoc(candidate.Node) @@ -27,7 +27,7 @@ func uniqueBy(d *dataTreeNavigator, context Context, expressionNode *ExpressionN if candidateNode.Kind != yaml.SequenceNode { return Context{}, fmt.Errorf("Only arrays are supported for unique") } - + var newMatches = orderedmap.NewOrderedMap() for _, node := range candidateNode.Content { child := &CandidateNode{Node: node} @@ -56,4 +56,4 @@ func uniqueBy(d *dataTreeNavigator, context Context, expressionNode *ExpressionN return context.ChildContext(results), nil -} \ No newline at end of file +} diff --git a/pkg/yqlib/operator_unique_by_test.go b/pkg/yqlib/operator_unique_by_test.go index 0f0eb4a3..0ca8e70f 100644 --- a/pkg/yqlib/operator_unique_by_test.go +++ b/pkg/yqlib/operator_unique_by_test.go @@ -6,9 +6,9 @@ import ( var uniqueOperatorScenarios = []expressionScenario{ { - description: "Unique array of scalars (string/numbers)", - document: `[1,2,3,2]`, - expression: `unique`, + description: "Unique array of scalars (string/numbers)", + document: `[1,2,3,2]`, + expression: `unique`, expected: []string{ "D0, P[], (!!seq)::- 1\n- 2\n- 3\n", }, @@ -16,8 +16,8 @@ var uniqueOperatorScenarios = []expressionScenario{ { description: "Unique nulls", subdescription: "Unique works on the node value, so it considers different representations of nulls to be different", - document: `[~,null, ~, null]`, - expression: `unique`, + document: `[~,null, ~, null]`, + expression: `unique`, expected: []string{ "D0, P[], (!!seq)::- ~\n- null\n", }, @@ -25,21 +25,20 @@ var uniqueOperatorScenarios = []expressionScenario{ { description: "Unique all nulls", subdescription: "Run against the node tag to unique all the nulls", - document: `[~,null, ~, null]`, - expression: `unique_by(tag)`, + document: `[~,null, ~, null]`, + expression: `unique_by(tag)`, expected: []string{ "D0, P[], (!!seq)::- ~\n", }, }, { - description: "Unique array object fields", - document: `[{name: harry, pet: cat}, {name: billy, pet: dog}, {name: harry, pet: dog}]`, - expression: `unique_by(.name)`, + description: "Unique array object fields", + document: `[{name: harry, pet: cat}, {name: billy, pet: dog}, {name: harry, pet: dog}]`, + expression: `unique_by(.name)`, expected: []string{ "D0, P[], (!!seq)::- {name: harry, pet: cat}\n- {name: billy, pet: dog}\n", }, }, - } func TestUniqueOperatorScenarios(t *testing.T) {