diff --git a/pkg/yqlib/doc/Has.md b/pkg/yqlib/doc/Has.md new file mode 100644 index 00000000..1f25009c --- /dev/null +++ b/pkg/yqlib/doc/Has.md @@ -0,0 +1,44 @@ +This is operation that returns true if the key exists in a map (or index in an array), false otherwise. +## Has map key +Given a sample.yml file of: +```yaml +- a: yes +- a: ~ +- a: +- b: nope +``` +then +```bash +yq eval '.[] | has("a")' sample.yml +``` +will output +```yaml +true +true +true +false +``` + +## Has array index +Given a sample.yml file of: +```yaml +- [] +- [1] +- [1, 2] +- [1, null] +- [1, 2, 3] + +``` +then +```bash +yq eval '.[] | has(1)' sample.yml +``` +will output +```yaml +false +false +true +true +true +``` + diff --git a/pkg/yqlib/doc/headers/Has.md b/pkg/yqlib/doc/headers/Has.md new file mode 100644 index 00000000..df898780 --- /dev/null +++ b/pkg/yqlib/doc/headers/Has.md @@ -0,0 +1 @@ +This is operation that returns true if the key exists in a map (or index in an array), false otherwise. \ No newline at end of file diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 837ae4f8..59fdace9 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -66,6 +66,7 @@ var Empty = &OperationType{Type: "EMPTY", NumArgs: 50, Handler: EmptyOperator} var RecursiveDescent = &OperationType{Type: "RECURSIVE_DESCENT", NumArgs: 0, Precedence: 50, Handler: RecursiveDescentOperator} var Select = &OperationType{Type: "SELECT", NumArgs: 1, Precedence: 50, Handler: SelectOperator} +var Has = &OperationType{Type: "HAS", NumArgs: 1, Precedence: 50, Handler: HasOperator} var DeleteChild = &OperationType{Type: "DELETE", NumArgs: 1, Precedence: 40, Handler: DeleteChildOperator} // var Exists = &OperationType{Type: "Length", NumArgs: 2, Precedence: 35} diff --git a/pkg/yqlib/operator_has.go b/pkg/yqlib/operator_has.go new file mode 100644 index 00000000..91e7a8e5 --- /dev/null +++ b/pkg/yqlib/operator_has.go @@ -0,0 +1,53 @@ +package yqlib + +import ( + "container/list" + "strconv" + + yaml "gopkg.in/yaml.v3" +) + +func HasOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { + + log.Debugf("-- hasOperation") + var results = list.New() + + rhs, err := d.GetMatchingNodes(matchingNodes, pathNode.Rhs) + wanted := rhs.Front().Value.(*CandidateNode).Node + wantedKey := wanted.Value + + if err != nil { + return nil, err + } + + for el := matchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + + // grab the first value + var contents = candidate.Node.Content + switch candidate.Node.Kind { + case yaml.MappingNode: + candidateHasKey := false + for index := 0; index < len(contents) && !candidateHasKey; index = index + 2 { + key := contents[index] + if key.Value == wantedKey { + candidateHasKey = true + } + } + results.PushBack(createBooleanCandidate(candidate, candidateHasKey)) + case yaml.SequenceNode: + candidateHasKey := false + if wanted.Tag == "!!int" { + var number, errParsingInt = strconv.ParseInt(wantedKey, 10, 64) // nolint + if errParsingInt != nil { + return nil, errParsingInt + } + candidateHasKey = int64(len(contents)) > number + } + results.PushBack(createBooleanCandidate(candidate, candidateHasKey)) + default: + results.PushBack(createBooleanCandidate(candidate, false)) + } + } + return results, nil +} diff --git a/pkg/yqlib/operator_has_test.go b/pkg/yqlib/operator_has_test.go new file mode 100644 index 00000000..dc0c3d2d --- /dev/null +++ b/pkg/yqlib/operator_has_test.go @@ -0,0 +1,48 @@ +package yqlib + +import ( + "testing" +) + +var hasOperatorScenarios = []expressionScenario{ + { + description: "Has map key", + document: `- a: "yes" +- a: ~ +- a: +- b: nope +`, + expression: `.[] | has("a")`, + expected: []string{ + "D0, P[0], (!!bool)::true\n", + "D0, P[1], (!!bool)::true\n", + "D0, P[2], (!!bool)::true\n", + "D0, P[3], (!!bool)::false\n", + }, + }, + { + dontFormatInputForDoc: true, + description: "Has array index", + document: `- [] +- [1] +- [1, 2] +- [1, null] +- [1, 2, 3] +`, + expression: `.[] | has(1)`, + expected: []string{ + "D0, P[0], (!!bool)::false\n", + "D0, P[1], (!!bool)::false\n", + "D0, P[2], (!!bool)::true\n", + "D0, P[3], (!!bool)::true\n", + "D0, P[4], (!!bool)::true\n", + }, + }, +} + +func TestHasOperatorScenarios(t *testing.T) { + for _, tt := range hasOperatorScenarios { + testScenario(t, &tt) + } + documentScenarios(t, "Has", hasOperatorScenarios) +} diff --git a/pkg/yqlib/path_tokeniser.go b/pkg/yqlib/path_tokeniser.go index 7f510b41..9d5d3b99 100644 --- a/pkg/yqlib/path_tokeniser.go +++ b/pkg/yqlib/path_tokeniser.go @@ -191,6 +191,7 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`:\s*`), opToken(CreateMap)) lexer.Add([]byte(`length`), opToken(Length)) lexer.Add([]byte(`select`), opToken(Select)) + lexer.Add([]byte(`has`), opToken(Has)) lexer.Add([]byte(`explode`), opToken(Explode)) lexer.Add([]byte(`or`), opToken(Or)) lexer.Add([]byte(`and`), opToken(And))