diff --git a/pkg/yqlib/doc/String Operators.md b/pkg/yqlib/doc/String Operators.md index 088bc5a9..4f1bf189 100644 --- a/pkg/yqlib/doc/String Operators.md +++ b/pkg/yqlib/doc/String Operators.md @@ -134,6 +134,24 @@ length: 3 captures: [] ``` +## Test using regex +Like jq'q equivalant, this works like match but only returns true/false instead of full match details + +Given a sample.yml file of: +```yaml +- cat +- dog +``` +then +```bash +yq eval '.[] | test("at")' sample.yml +``` +will output +```yaml +true +false +``` + ## Substitute / Replace string This uses golang regex, described [here](https://github.com/google/re2/wiki/Syntax) Note the use of `|=` to run in context of the current string value. diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index 856c49c9..691b44dd 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -277,6 +277,7 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`join`), opToken(joinStringOpType)) lexer.Add([]byte(`sub`), opToken(subStringOpType)) lexer.Add([]byte(`match`), opToken(matchOpType)) + lexer.Add([]byte(`test`), opToken(testOpType)) lexer.Add([]byte(`any`), opToken(anyOpType)) lexer.Add([]byte(`any_c`), opToken(anyConditionOpType)) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 4a35554b..2f714160 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -84,6 +84,7 @@ var sortKeysOpType = &operationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 5 var joinStringOpType = &operationType{Type: "JOIN", NumArgs: 1, Precedence: 50, Handler: joinStringOperator} var subStringOpType = &operationType{Type: "SUBSTR", NumArgs: 1, Precedence: 50, Handler: substituteStringOperator} var matchOpType = &operationType{Type: "MATCH", NumArgs: 1, Precedence: 50, Handler: matchOperator} +var testOpType = &operationType{Type: "MATCH", NumArgs: 1, Precedence: 50, Handler: testOperator} var splitStringOpType = &operationType{Type: "SPLIT", NumArgs: 1, Precedence: 50, Handler: splitStringOperator} var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 50, Handler: keysOperator} diff --git a/pkg/yqlib/operator_strings.go b/pkg/yqlib/operator_strings.go index a363a9ce..16c811bb 100644 --- a/pkg/yqlib/operator_strings.go +++ b/pkg/yqlib/operator_strings.go @@ -112,6 +112,14 @@ func match(matchPrefs matchPreferences, regEx *regexp.Regexp, candidate *Candida allIndices = [][]int{regEx.FindStringSubmatchIndex(value)} } + log.Debug("allMatches, %v", allMatches) + + // if all matches just has an empty array in it, + // then nothing matched + if len(allMatches) > 0 && len(allMatches[0]) == 0 { + return + } + for i, matches := range allMatches { capturesNode := &yaml.Node{Kind: yaml.SequenceNode} match, submatches := matches[0], matches[1:] @@ -133,7 +141,7 @@ func match(matchPrefs matchPreferences, regEx *regexp.Regexp, candidate *Candida } -func extractMatchArguments(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (string, matchPreferences, error) { +func extractMatchArguments(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (*regexp.Regexp, matchPreferences, error) { regExExpNode := expressionNode.Rhs matchPrefs := matchPreferences{} @@ -144,7 +152,7 @@ func extractMatchArguments(d *dataTreeNavigator, context Context, expressionNode regExExpNode = block.Lhs replacementNodes, err := d.GetMatchingNodes(context, block.Rhs) if err != nil { - return "", matchPrefs, err + return nil, matchPrefs, err } paramText := "" if replacementNodes.MatchingNodes.Front() != nil { @@ -155,16 +163,16 @@ func extractMatchArguments(d *dataTreeNavigator, context Context, expressionNode matchPrefs.Global = true } if strings.Contains(paramText, "i") { - return "", matchPrefs, fmt.Errorf(`'i' is not a valid option for match. To ignore case, use an expression like match("(?i)cat")`) + return nil, matchPrefs, fmt.Errorf(`'i' is not a valid option for match. To ignore case, use an expression like match("(?i)cat")`) } if len(paramText) > 0 { - return "", matchPrefs, fmt.Errorf(`Unrecognised match params '%v', please see docs at https://mikefarah.gitbook.io/yq/operators/string-operators`, paramText) + return nil, matchPrefs, fmt.Errorf(`Unrecognised match params '%v', please see docs at https://mikefarah.gitbook.io/yq/operators/string-operators`, paramText) } } regExNodes, err := d.GetMatchingNodes(context.ReadOnlyClone(), regExExpNode) if err != nil { - return "", matchPrefs, err + return nil, matchPrefs, err } log.Debug(NodesToString(regExNodes.MatchingNodes)) regExStr := "" @@ -172,16 +180,12 @@ func extractMatchArguments(d *dataTreeNavigator, context Context, expressionNode regExStr = regExNodes.MatchingNodes.Front().Value.(*CandidateNode).Node.Value } log.Debug("regEx %v", regExStr) - return regExStr, matchPrefs, nil + regEx, err := regexp.Compile(regExStr) + return regEx, matchPrefs, err } func matchOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { - regExStr, matchPrefs, err := extractMatchArguments(d, context, expressionNode) - if err != nil { - return Context{}, err - } - - regEx, err := regexp.Compile(regExStr) + regEx, matchPrefs, err := extractMatchArguments(d, context, expressionNode) if err != nil { return Context{}, err } @@ -201,6 +205,28 @@ func matchOperator(d *dataTreeNavigator, context Context, expressionNode *Expres return context.ChildContext(results), nil } +func testOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + regEx, _, err := extractMatchArguments(d, context, expressionNode) + if err != nil { + return Context{}, err + } + + var results = list.New() + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + node := unwrapDoc(candidate.Node) + if node.Tag != "!!str" { + return Context{}, fmt.Errorf("cannot match with %v, can only match strings. Hint: Most often you'll want to use '|=' over '=' for this operation", node.Tag) + } + matches := regEx.FindStringSubmatch(node.Value) + results.PushBack(createBooleanCandidate(candidate, len(matches) > 0)) + + } + + return context.ChildContext(results), nil +} + func joinStringOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { log.Debugf("-- joinStringOperator") joinStr := "" diff --git a/pkg/yqlib/operator_strings_test.go b/pkg/yqlib/operator_strings_test.go index 0199ce52..cc28371a 100644 --- a/pkg/yqlib/operator_strings_test.go +++ b/pkg/yqlib/operator_strings_test.go @@ -62,6 +62,35 @@ var stringsOperatorScenarios = []expressionScenario{ "D0, P[], ()::string: cat\noffset: 4\nlength: 3\ncaptures: []\n", }, }, + { + skipDoc: true, + description: "No match", + document: `dog`, + expression: `match("cat"; "g")`, + expected: []string{}, + }, + { + skipDoc: true, + description: "No match", + expression: `"dog" | match("cat", "g")`, + expected: []string{}, + }, + { + skipDoc: true, + description: "No match", + expression: `"dog" | match("cat")`, + expected: []string{}, + }, + { + description: "Test using regex", + subdescription: "Like jq'q equivalant, this works like match but only returns true/false instead of full match details", + document: `["cat", "dog"]`, + expression: `.[] | test("at")`, + expected: []string{ + "D0, P[0], (!!bool)::true\n", + "D0, P[1], (!!bool)::false\n", + }, + }, { description: "Substitute / Replace string", subdescription: "This uses golang regex, described [here](https://github.com/google/re2/wiki/Syntax)\nNote the use of `|=` to run in context of the current string value.",