diff --git a/pkg/yqlib/doc/Keys.md b/pkg/yqlib/doc/Keys.md new file mode 100644 index 0000000..b92293e --- /dev/null +++ b/pkg/yqlib/doc/Keys.md @@ -0,0 +1,38 @@ + +## Get Keys +Given a sample.yml file of: +```yaml +a: + b: cat + c: dog + d: frog +``` +then +```bash +yq eval '.a | keys' sample.yml +``` +will output +```yaml +3 +``` + +## Set key style +Given a sample.yml file of: +```yaml +a: + b: cat + c: dog + d: frog +``` +then +```bash +yq eval '(.a | keys) style = 'single'' sample.yml +``` +will output +```yaml +a: + b: cat + c: dog + d: frog +``` + diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 56ae648..f2b93bf 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -49,6 +49,7 @@ var CreateMap = &OperationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 40, H var ShortPipe = &OperationType{Type: "SHORT_PIPE", NumArgs: 2, Precedence: 45, Handler: PipeOperator} var Length = &OperationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: LengthOperator} +var Keys = &OperationType{Type: "KEYS", NumArgs: 0, Precedence: 50, Handler: KeysOperator} var Collect = &OperationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: CollectOperator} var GetStyle = &OperationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: GetStyleOperator} var GetTag = &OperationType{Type: "GET_TAG", NumArgs: 0, Precedence: 50, Handler: GetTagOperator} diff --git a/pkg/yqlib/operator_keys.go b/pkg/yqlib/operator_keys.go new file mode 100644 index 0000000..3aa455b --- /dev/null +++ b/pkg/yqlib/operator_keys.go @@ -0,0 +1,34 @@ +package yqlib + +import ( + "container/list" + "fmt" + + yaml "gopkg.in/yaml.v3" +) + +func KeysOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { + log.Debugf("-- KeyOperation") + var results = list.New() + + for el := matchMap.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + targetNode := UnwrapDoc(candidate.Node) + + switch targetNode.Kind { + case yaml.MappingNode: + node := &yaml.Node{Kind: yaml.SequenceNode, Value: fmt.Sprintf("%v", length), Tag: "!!int"} + + case yaml.SequenceNode: + length = len(targetNode.Content) + default: + length = 0 + } + + node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", length), Tag: "!!int"} + lengthCand := &CandidateNode{Node: node, Document: candidate.Document, Path: candidate.Path} + results.PushBack(lengthCand) + } + + return results, nil +} diff --git a/pkg/yqlib/operator_keys_test.go b/pkg/yqlib/operator_keys_test.go new file mode 100644 index 0000000..93e788f --- /dev/null +++ b/pkg/yqlib/operator_keys_test.go @@ -0,0 +1,88 @@ +package yqlib + +import ( + "testing" +) + +var keyOperatorScenarios = []expressionScenario{ + { + description: "Get Keys of map", + document: `{a: {b: cat, c: dog, d: frog}}`, + expression: `.a | keys`, + expected: []string{ + "D0, P[a], (!!seq)::- b\n- c\n- d\n", + }, + }, + { + skipDoc: true, + document: `{a1: {b: cat, c: dog, d: frog}, a2: {e: cat, f: dog, g: frog}}`, + expression: `(.a1, .a2) | keys`, + expected: []string{ + "D0, P[a1], (!!seq)::- b\n- c\n- d\n", + "D0, P[a2], (!!seq)::- e\n- f\n- g\n", + }, + }, + { + description: "Get Keys of array", + document: `{a: [0,1,2]}`, + expression: `.a | keys`, + expected: []string{ + "D0, P[a], (!!seq)::0\n", + "D0, P[a], (!!int)::1\n", + "D0, P[a], (!!int)::2\n", + }, + }, + { + description: "Set key style", + document: `{a: {b: cat, c: dog, d: frog}}`, + expression: `(.a | keys | ..) style = 'single'`, + expected: []string{ + "D0, P[], (!!doc)::{a: {'b': cat, 'c': dog, 'd': frog}}\n", + }, + }, + { + description: "Set key alias", + document: `{a: {b: cat, c: dog, d: frog}}`, + expression: `(.a | keys | .. | select(.=="b")) alias = 'boo'`, + expected: []string{ + "D0, P[], (!!doc)::{a: {*meow: cat, c: dog, d: frog}}\n", + }, + }, + { + description: "Set key alias", + document: `{a: {b: cat, c: dog, d: frog}}`, + expression: `(.a | key("b")) alias = 'boo'`, + expected: []string{ + "D0, P[], (!!doc)::{a: {*meow: cat, c: dog, d: frog}}\n", + }, + }, + { + skipDoc: true, + document: `{a: [0,1,2]}`, + expression: `(.a | keys) style = 'single'`, + expected: []string{ + "D0, P[a], (!!int)::0\n", + "D0, P[a], (!!int)::1\n", + "D0, P[a], (!!int)::2\n", + }, + }, + { + skipDoc: true, + document: `a: cat`, + expression: `.a | keys`, + expected: []string{}, + }, + { + skipDoc: true, + document: `{}`, + expression: `.a | keys`, + expected: []string{}, + }, +} + +func TestKeyOperatorScenarios(t *testing.T) { + for _, tt := range keyOperatorScenarios { + testScenario(t, &tt) + } + documentScenarios(t, "Keys", keyOperatorScenarios) +} diff --git a/pkg/yqlib/path_tokeniser.go b/pkg/yqlib/path_tokeniser.go index 9a6b1ed..39c8f7a 100644 --- a/pkg/yqlib/path_tokeniser.go +++ b/pkg/yqlib/path_tokeniser.go @@ -182,6 +182,7 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`,`), opToken(Union)) lexer.Add([]byte(`:\s*`), opToken(CreateMap)) lexer.Add([]byte(`length`), opToken(Length)) + lexer.Add([]byte(`keys`), opToken(Keys)) lexer.Add([]byte(`sortKeys`), opToken(SortKeys)) lexer.Add([]byte(`select`), opToken(Select)) lexer.Add([]byte(`has`), opToken(Has)) diff --git a/pkg/yqlib/test.yml b/pkg/yqlib/test.yml new file mode 100644 index 0000000..4106642 --- /dev/null +++ b/pkg/yqlib/test.yml @@ -0,0 +1,10 @@ +{ + "a1": { + "b": "cat", + "c": "dog" + }, + "a2": { + "d": "cat", + "e": "dog" + } +} \ No newline at end of file