diff --git a/pkg/yqlib/decoder_properties.go b/pkg/yqlib/decoder_properties.go index 4acbdf95..fdddc596 100644 --- a/pkg/yqlib/decoder_properties.go +++ b/pkg/yqlib/decoder_properties.go @@ -16,10 +16,11 @@ type propertiesDecoder struct { reader io.Reader finished bool d DataTreeNavigator + prefs PropertiesPreferences } func NewPropertiesDecoder() Decoder { - return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false} + return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false, prefs: ConfiguredPropertiesPreferences.Copy()} } func (dec *propertiesDecoder) Init(reader io.Reader) error { @@ -28,20 +29,56 @@ func (dec *propertiesDecoder) Init(reader io.Reader) error { return nil } -func parsePropKey(key string) []interface{} { +func parsePropKey(key string, prefs PropertiesPreferences) []interface{} { pathStrArray := strings.Split(key, ".") - path := make([]interface{}, len(pathStrArray)) - for i, pathStr := range pathStrArray { - num, err := strconv.ParseInt(pathStr, 10, 32) - if err == nil { - path[i] = num - } else { - path[i] = pathStr - } + path := make([]interface{}, 0, len(pathStrArray)) + for _, pathStr := range pathStrArray { + path = appendPropKeySegment(path, pathStr, prefs.UseArrayBrackets) } return path } +func appendPropKeySegment(path []interface{}, segment string, useArrayBrackets bool) []interface{} { + if useArrayBrackets && strings.Contains(segment, "[") { + bracketPath, ok := parsePropKeyArrayBracketSegment(segment) + if ok { + return append(path, bracketPath...) + } + } + + num, err := strconv.ParseInt(segment, 10, 32) + if err == nil { + return append(path, num) + } + return append(path, segment) +} + +func parsePropKeyArrayBracketSegment(segment string) ([]interface{}, bool) { + path := []interface{}{} + bracketIndex := strings.Index(segment, "[") + if bracketIndex > 0 { + path = append(path, segment[:bracketIndex]) + } + + remaining := segment[bracketIndex:] + for remaining != "" { + if !strings.HasPrefix(remaining, "[") { + return nil, false + } + closingBracket := strings.Index(remaining, "]") + if closingBracket < 0 { + return nil, false + } + arrayIndex, err := strconv.ParseInt(remaining[1:closingBracket], 10, 32) + if err != nil { + return nil, false + } + path = append(path, arrayIndex) + remaining = remaining[closingBracket+1:] + } + return path, true +} + func (dec *propertiesDecoder) processComment(c string) string { if c == "" { return "" @@ -75,7 +112,7 @@ func (dec *propertiesDecoder) applyPropertyComments(context Context, path []inte func (dec *propertiesDecoder) applyProperty(context Context, properties *properties.Properties, key string) error { value, _ := properties.Get(key) - path := parsePropKey(key) + path := parsePropKey(key, dec.prefs) propertyComments := properties.GetComments(key) if len(propertyComments) > 0 { diff --git a/pkg/yqlib/properties_test.go b/pkg/yqlib/properties_test.go index 4f2ba8e6..7a59b018 100644 --- a/pkg/yqlib/properties_test.go +++ b/pkg/yqlib/properties_test.go @@ -202,6 +202,37 @@ var propertyScenarios = []formatScenario{ expected: expectedDecodedYaml, scenarioType: "decode", }, + { + skipDoc: true, + description: "Decode properties with array brackets", + input: `user.credentials[0].username=user1 +user.credentials[0].password=$2b$08$... +user.credentials[1].username=user2 +user.credentials[1].password=$2b$08$... +user.credentials[2].username=user3 +user.credentials[2].password=$2b$10$...`, + expected: `user: + credentials: + - username: user1 + password: $2b$08$... + - username: user2 + password: $2b$08$... + - username: user3 + password: $2b$10$... +`, + scenarioType: "decode-array-brackets", + }, + { + skipDoc: true, + description: "Decode properties with nested array brackets", + input: `user.clowns[0][1] = "cool"`, + expected: `user: + clowns: + - - null + - '"cool"' +`, + scenarioType: "decode-array-brackets", + }, { skipDoc: true, @@ -442,6 +473,12 @@ func TestPropertyScenarios(t *testing.T) { test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(ConfiguredPropertiesPreferences)), s.description) case "decode": test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)), s.description) + case "decode-array-brackets": + previousPreferences := ConfiguredPropertiesPreferences.Copy() + ConfiguredPropertiesPreferences.UseArrayBrackets = true + actual := mustProcessFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)) + ConfiguredPropertiesPreferences = previousPreferences + test.AssertResultWithContext(t, s.expected, actual, s.description) case "encode-wrapped": prefs := ConfiguredPropertiesPreferences.Copy() prefs.UnwrapScalar = false