From bdeedbd2759ee48da7ededfd674266219f78bdc2 Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Sat, 31 Jan 2026 15:25:11 +1100 Subject: [PATCH] Fixing TOML subarray parsing issue #2581 --- pkg/yqlib/decoder_toml.go | 49 ++++++++++++++++++-------- pkg/yqlib/doc/usage/toml.md | 55 +++++++++++++++++++++++++++++ pkg/yqlib/toml_test.go | 70 +++++++++++++++++++++++-------------- project-words.txt | 1 + 4 files changed, 134 insertions(+), 41 deletions(-) diff --git a/pkg/yqlib/decoder_toml.go b/pkg/yqlib/decoder_toml.go index bd58c60f..8e76d944 100644 --- a/pkg/yqlib/decoder_toml.go +++ b/pkg/yqlib/decoder_toml.go @@ -430,23 +430,42 @@ func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error) // Because TOML. So we'll inject the last index into the path. func getPathToUse(fullPath []interface{}, dec *tomlDecoder, c Context) ([]interface{}, error) { - pathToCheck := fullPath - if len(fullPath) >= 1 { - pathToCheck = fullPath[:len(fullPath)-1] - } - readOp := createTraversalTree(pathToCheck, traversePreferences{DontAutoCreate: true}, false) + // We need to check the entire path (except the last element), not just the immediate parent, + // because we may have nested array tables like [[array.subarray.subsubarray]] + // where both 'array' and 'subarray' are arrays that already exist. - resultContext, err := dec.d.GetMatchingNodes(c, readOp) - if err != nil { - return nil, err + if len(fullPath) == 0 { + return fullPath, nil } - if resultContext.MatchingNodes.Len() >= 1 { - match := resultContext.MatchingNodes.Front().Value.(*CandidateNode) - // path refers to an array, we need to add this to the last element in the array - if match.Kind == SequenceNode { - fullPath = append(pathToCheck, len(match.Content)-1, fullPath[len(fullPath)-1]) - log.Debugf("Adding to end of %v array, using path: %v", pathToCheck, fullPath) + + resultPath := make([]interface{}, 0, len(fullPath)*2) // preallocate with extra space for indices + + // Process all segments except the last one + for i := 0; i < len(fullPath)-1; i++ { + resultPath = append(resultPath, fullPath[i]) + + // Check if the current path segment points to an array + readOp := createTraversalTree(resultPath, traversePreferences{DontAutoCreate: true}, false) + resultContext, err := dec.d.GetMatchingNodes(c, readOp) + if err != nil { + return nil, err + } + + if resultContext.MatchingNodes.Len() >= 1 { + match := resultContext.MatchingNodes.Front().Value.(*CandidateNode) + // If this segment points to an array, we need to add the last index + // before continuing with the rest of the path + if match.Kind == SequenceNode && len(match.Content) > 0 { + lastIndex := len(match.Content) - 1 + resultPath = append(resultPath, lastIndex) + log.Debugf("Path segment %v is an array, injecting index %d", resultPath[:len(resultPath)-1], lastIndex) + } } } - return fullPath, err + + // Add the last segment + resultPath = append(resultPath, fullPath[len(fullPath)-1]) + + log.Debugf("getPathToUse: original path %v -> result path %v", fullPath, resultPath) + return resultPath, nil } diff --git a/pkg/yqlib/doc/usage/toml.md b/pkg/yqlib/doc/usage/toml.md index f0aec270..45402b47 100644 --- a/pkg/yqlib/doc/usage/toml.md +++ b/pkg/yqlib/doc/usage/toml.md @@ -329,3 +329,58 @@ B = 12 name = "Tom" # name comment ``` +## Roundtrip: sample from web +Given a sample.toml file of: +```toml +# This is a TOML document +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +dob = 1979-05-27T07:32:00-08:00 + +[database] +enabled = true +ports = [8000, 8001, 8002] +data = [["delta", "phi"], [3.14]] +temp_targets = { cpu = 79.5, case = 72.0 } + +# [servers] yq can't do this one yet +[servers.alpha] +ip = "10.0.0.1" +role = "frontend" + +[servers.beta] +ip = "10.0.0.2" +role = "backend" + +``` +then +```bash +yq '.' sample.toml +``` +will output +```yaml +# This is a TOML document +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +dob = 1979-05-27T07:32:00-08:00 + +[database] +enabled = true +ports = [8000, 8001, 8002] +data = [["delta", "phi"], [3.14]] +temp_targets = { cpu = 79.5, case = 72.0 } + +# [servers] yq can't do this one yet +[servers.alpha] +ip = "10.0.0.1" +role = "frontend" + +[servers.beta] +ip = "10.0.0.2" +role = "backend" +``` + diff --git a/pkg/yqlib/toml_test.go b/pkg/yqlib/toml_test.go index 23ccf8e1..4c2ef336 100644 --- a/pkg/yqlib/toml_test.go +++ b/pkg/yqlib/toml_test.go @@ -228,31 +228,42 @@ B = 12 name = "Tom" # name comment ` -// var sampleFromWeb = ` -// # This is a TOML document +var sampleFromWeb = `# This is a TOML document +title = "TOML Example" -// title = "TOML Example" +[owner] +name = "Tom Preston-Werner" +dob = 1979-05-27T07:32:00-08:00 -// [owner] -// name = "Tom Preston-Werner" -// dob = 1979-05-27T07:32:00-08:00 +[database] +enabled = true +ports = [8000, 8001, 8002] +data = [["delta", "phi"], [3.14]] +temp_targets = { cpu = 79.5, case = 72.0 } -// [database] -// enabled = true -// ports = [8000, 8001, 8002] -// data = [["delta", "phi"], [3.14]] -// temp_targets = { cpu = 79.5, case = 72.0 } +# [servers] yq can't do this one yet +[servers.alpha] +ip = "10.0.0.1" +role = "frontend" -// [servers] +[servers.beta] +ip = "10.0.0.2" +role = "backend" +` -// [servers.alpha] -// ip = "10.0.0.1" -// role = "frontend" +var subArrays = ` +[[array]] -// [servers.beta] -// ip = "10.0.0.2" -// role = "backend" -// ` +[[array.subarray]] + +[[array.subarray.subsubarray]] +` + +var expectedSubArrays = `array: + - subarray: + - subsubarray: + - {} +` var tomlScenarios = []formatScenario{ { @@ -461,6 +472,13 @@ var tomlScenarios = []formatScenario{ expected: expectedMultipleEmptyTables, scenarioType: "decode", }, + { + description: "subArrays", + skipDoc: true, + input: subArrays, + expected: expectedSubArrays, + scenarioType: "decode", + }, // Roundtrip scenarios { description: "Roundtrip: inline table attribute", @@ -532,13 +550,13 @@ var tomlScenarios = []formatScenario{ expected: rtComments, scenarioType: "roundtrip", }, - // { - // description: "Roundtrip: sample from web", - // input: sampleFromWeb, - // expression: ".", - // expected: sampleFromWeb, - // scenarioType: "roundtrip", - // }, + { + description: "Roundtrip: sample from web", + input: sampleFromWeb, + expression: ".", + expected: sampleFromWeb, + scenarioType: "roundtrip", + }, } func testTomlScenario(t *testing.T, s formatScenario) { diff --git a/project-words.txt b/project-words.txt index 759f0dd5..8f5dcece 100644 --- a/project-words.txt +++ b/project-words.txt @@ -293,3 +293,4 @@ buildvcs behaviour GOFLAGS gocache +subsubarray \ No newline at end of file