diff --git a/pkg/yqlib/doc/Entries.md b/pkg/yqlib/doc/Entries.md index 9b91eb97..1acfbddf 100644 --- a/pkg/yqlib/doc/Entries.md +++ b/pkg/yqlib/doc/Entries.md @@ -35,3 +35,37 @@ will output value: b ``` +## from_entries map +Given a sample.yml file of: +```yaml +a: 1 +b: 2 +``` +then +```bash +yq eval 'to_entries | from_entries' sample.yml +``` +will output +```yaml +a: 1 +b: 2 +``` + +## from_entries with numeric key indexes +from_entries always creates a map, even for numeric keys + +Given a sample.yml file of: +```yaml +- a +- b +``` +then +```bash +yq eval 'to_entries | from_entries' sample.yml +``` +will output +```yaml +0: a +1: b +``` + diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index 6d5a8d85..3b6c7e81 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -279,6 +279,7 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`fi`), opToken(getFileIndexOpType)) lexer.Add([]byte(`path`), opToken(getPathOpType)) lexer.Add([]byte(`to_entries`), opToken(toEntriesOpType)) + lexer.Add([]byte(`from_entries`), opToken(fromEntriesOpType)) lexer.Add([]byte(`lineComment`), opTokenWithPrefs(getCommentOpType, assignCommentOpType, commentOpPreferences{LineComment: true})) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 3f0b074b..0a64ecc8 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -61,6 +61,7 @@ var shortPipeOpType = &operationType{Type: "SHORT_PIPE", NumArgs: 2, Precedence: var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator} var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50, Handler: collectOperator} var toEntriesOpType = &operationType{Type: "TO_ENTRIES", NumArgs: 0, Precedence: 50, Handler: toEntriesOperator} +var fromEntriesOpType = &operationType{Type: "TO_ENTRIES", NumArgs: 0, Precedence: 50, Handler: fromEntriesOperator} var splitDocumentOpType = &operationType{Type: "SPLIT_DOC", NumArgs: 0, Precedence: 50, Handler: splitDocumentOperator} var getVariableOpType = &operationType{Type: "GET_VARIABLE", NumArgs: 0, Precedence: 55, Handler: getVariableOperator} var getStyleOpType = &operationType{Type: "GET_STYLE", NumArgs: 0, Precedence: 50, Handler: getStyleOperator} diff --git a/pkg/yqlib/operator_entries.go b/pkg/yqlib/operator_entries.go index 0140180e..e166ed6e 100644 --- a/pkg/yqlib/operator_entries.go +++ b/pkg/yqlib/operator_entries.go @@ -63,5 +63,66 @@ func toEntriesOperator(d *dataTreeNavigator, context Context, expressionNode *Ex } return context.ChildContext(results), nil +} +func parseEntry(d *dataTreeNavigator, entry *yaml.Node, position int) (*yaml.Node, *yaml.Node, error) { + prefs := traversePreferences{DontAutoCreate: true} + candidateNode := &CandidateNode{Node: entry} + + keyResults, err := traverseMap(Context{}, candidateNode, "key", prefs, false) + + if err != nil { + return nil, nil, err + } else if keyResults.Len() != 1 { + return nil, nil, fmt.Errorf("Expected to find one 'key' entry but found %v in position %v", keyResults.Len(), position) + } + + valueResults, err := traverseMap(Context{}, candidateNode, "value", prefs, false) + + if err != nil { + return nil, nil, err + } else if valueResults.Len() != 1 { + return nil, nil, fmt.Errorf("Expected to find one 'value' entry but found %v in position %v", valueResults.Len(), position) + } + + return keyResults.Front().Value.(*CandidateNode).Node, valueResults.Front().Value.(*CandidateNode).Node, nil + +} + +func fromEntries(d *dataTreeNavigator, candidateNode * CandidateNode) (*CandidateNode, error) { + var node = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + var mapCandidateNode = candidateNode.CreateChild(nil, node) + + var contents = unwrapDoc(candidateNode.Node).Content + + for index := 0; index < len(contents); index = index + 1 { + key, value, err := parseEntry(d, contents[index], index) + if err != nil { + return nil, err + } + + node.Content = append(node.Content, key, value) + } + return mapCandidateNode, nil +} + +func fromEntriesOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + var results = list.New() + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + candidateNode := unwrapDoc(candidate.Node) + + switch candidateNode.Kind { + case yaml.SequenceNode: + mapResult, err :=fromEntries(d, candidate) + if err != nil { + return Context{}, err + } + results.PushBack(mapResult) + default: + return Context{}, fmt.Errorf("from entries only runs against arrays") + } + } + + return context.ChildContext(results), nil } \ No newline at end of file diff --git a/pkg/yqlib/operator_entries_test.go b/pkg/yqlib/operator_entries_test.go index 3aafd2e9..1bb4755d 100644 --- a/pkg/yqlib/operator_entries_test.go +++ b/pkg/yqlib/operator_entries_test.go @@ -21,6 +21,23 @@ var entriesOperatorScenarios = []expressionScenario{ "D0, P[], (!!seq)::- key: 0\n value: a\n- key: 1\n value: b\n", }, }, + { + description: "from_entries map", + document: `{a: 1, b: 2}`, + expression: `to_entries | from_entries`, + expected: []string{ + "D0, P[], (!!map)::a: 1\nb: 2\n", + }, + }, + { + description: "from_entries with numeric key indexes", + subdescription: "from_entries always creates a map, even for numeric keys", + document: `[a,b]`, + expression: `to_entries | from_entries`, + expected: []string{ + "D0, P[], (!!map)::0: a\n1: b\n", + }, + }, } func TestEntriesOperatorScenarios(t *testing.T) {