diff --git a/pkg/yqlib/doc/Keys.md b/pkg/yqlib/doc/Keys.md new file mode 100644 index 00000000..04bad8b2 --- /dev/null +++ b/pkg/yqlib/doc/Keys.md @@ -0,0 +1,35 @@ +# Keys + +Use the `keys` operator to return map keys or array indices. +## Map keys +Given a sample.yml file of: +```yaml +dog: woof +cat: meow +``` +then +```bash +yq eval 'keys' sample.yml +``` +will output +```yaml +- dog +- cat +``` + +## Array keys +Given a sample.yml file of: +```yaml +- apple +- banana +``` +then +```bash +yq eval 'keys' sample.yml +``` +will output +```yaml +- 0 +- 1 +``` + diff --git a/pkg/yqlib/doc/headers/Keys.md b/pkg/yqlib/doc/headers/Keys.md new file mode 100644 index 00000000..ba8c9992 --- /dev/null +++ b/pkg/yqlib/doc/headers/Keys.md @@ -0,0 +1,3 @@ +# Keys + +Use the `keys` operator to return map keys or array indices. \ No newline at end of file diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index 934cd03e..26bd53e9 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -245,6 +245,7 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`join`), opToken(joinStringOpType)) lexer.Add([]byte(`split`), opToken(splitStringOpType)) + lexer.Add([]byte(`keys`), opToken(keysOpType)) lexer.Add([]byte(`style`), opAssignableToken(getStyleOpType, assignStyleOpType)) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 0798d05a..dcaa614a 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -67,6 +67,8 @@ var sortKeysOpType = &operationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 5 var joinStringOpType = &operationType{Type: "JOIN", NumArgs: 1, Precedence: 50, Handler: joinStringOperator} var splitStringOpType = &operationType{Type: "SPLIT", NumArgs: 1, Precedence: 50, Handler: splitStringOperator} +var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 50, Handler: keysOperator} + var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator} var traversePathOpType = &operationType{Type: "TRAVERSE_PATH", NumArgs: 0, Precedence: 50, Handler: traversePathOperator} var traverseArrayOpType = &operationType{Type: "TRAVERSE_ARRAY", NumArgs: 1, Precedence: 50, Handler: traverseArrayOperator} diff --git a/pkg/yqlib/operator_keys.go b/pkg/yqlib/operator_keys.go new file mode 100644 index 00000000..b778355f --- /dev/null +++ b/pkg/yqlib/operator_keys.go @@ -0,0 +1,54 @@ +package yqlib + +import ( + "container/list" + "fmt" + + "gopkg.in/yaml.v3" +) + +func keysOperator(d *dataTreeNavigator, matchMap *list.List, expressionNode *ExpressionNode) (*list.List, error) { + log.Debugf("-- keysOperator") + + var results = list.New() + + for el := matchMap.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + node := unwrapDoc(candidate.Node) + var targetNode *yaml.Node + if node.Kind == yaml.MappingNode { + targetNode = getMapKeys(node) + } else if node.Kind == yaml.SequenceNode { + targetNode = getIndicies(node) + } else { + return nil, fmt.Errorf("Cannot get keys of %v, keys only works for maps and arrays", node.Tag) + } + + result := candidate.CreateChild(nil, targetNode) + results.PushBack(result) + } + + return results, nil +} + +func getMapKeys(node *yaml.Node) *yaml.Node { + contents := make([]*yaml.Node, 0) + for index := 0; index < len(node.Content); index = index + 2 { + contents = append(contents, node.Content[index]) + } + return &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Content: contents} +} + +func getIndicies(node *yaml.Node) *yaml.Node { + var contents = make([]*yaml.Node, len(node.Content)) + + for index := range node.Content { + contents[index] = &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!int", + Value: fmt.Sprintf("%v", index), + } + } + + return &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Content: contents} +} diff --git a/pkg/yqlib/operator_keys_test.go b/pkg/yqlib/operator_keys_test.go new file mode 100644 index 00000000..91bd649e --- /dev/null +++ b/pkg/yqlib/operator_keys_test.go @@ -0,0 +1,47 @@ +package yqlib + +import ( + "testing" +) + +var keysOperatorScenarios = []expressionScenario{ + { + description: "Map keys", + document: `{dog: woof, cat: meow}`, + expression: `keys`, + expected: []string{ + "D0, P[], (!!seq)::- dog\n- cat\n", + }, + }, + { + skipDoc: true, + document: `{}`, + expression: `keys`, + expected: []string{ + "D0, P[], (!!seq)::[]\n", + }, + }, + { + description: "Array keys", + document: `[apple, banana]`, + expression: `keys`, + expected: []string{ + "D0, P[], (!!seq)::- 0\n- 1\n", + }, + }, + { + skipDoc: true, + document: `[]`, + expression: `keys`, + expected: []string{ + "D0, P[], (!!seq)::[]\n", + }, + }, +} + +func TestKeysOperatorScenarios(t *testing.T) { + for _, tt := range keysOperatorScenarios { + testScenario(t, &tt) + } + documentScenarios(t, "Keys", keysOperatorScenarios) +}