diff --git a/cmd/root.go b/cmd/root.go index d2f72bfa..a7cf7354 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -156,6 +156,8 @@ yq -P -oy sample.json rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.UnquotedKeys, "lua-unquoted", yqlib.ConfiguredLuaPreferences.UnquotedKeys, "output unquoted string keys (e.g. {foo=\"bar\"})") rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.Globals, "lua-globals", yqlib.ConfiguredLuaPreferences.Globals, "output keys as top-level global variables") + rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredINIPreferences.PreserveSurroundedQuote, "ini-preserve-quotes", yqlib.ConfiguredINIPreferences.PreserveSurroundedQuote, "preserve surrounding quotes on INI values during round-trip") + rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "properties-separator", yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "separator to use between keys and values") rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "properties-array-brackets", yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "use [x] in array paths (e.g. for SpringBoot)") diff --git a/pkg/yqlib/decoder_ini.go b/pkg/yqlib/decoder_ini.go index 159ca8b4..e536edb2 100644 --- a/pkg/yqlib/decoder_ini.go +++ b/pkg/yqlib/decoder_ini.go @@ -12,11 +12,13 @@ import ( type iniDecoder struct { reader io.Reader finished bool // Flag to signal completion of processing + prefs INIPreferences } -func NewINIDecoder() Decoder { +func NewINIDecoder(prefs INIPreferences) Decoder { return &iniDecoder{ finished: false, // Initialise the flag as false + prefs: prefs, } } @@ -39,7 +41,10 @@ func (dec *iniDecoder) Decode() (*CandidateNode, error) { } // Parse the INI content - cfg, err := ini.Load(content) + loadOpts := ini.LoadOptions{ + PreserveSurroundedQuote: dec.prefs.PreserveSurroundedQuote, + } + cfg, err := ini.LoadSources(loadOpts, content) if err != nil { return nil, fmt.Errorf("failed to parse INI content: %w", err) } diff --git a/pkg/yqlib/format.go b/pkg/yqlib/format.go index 4cf456f7..9cccf367 100644 --- a/pkg/yqlib/format.go +++ b/pkg/yqlib/format.go @@ -90,7 +90,7 @@ var LuaFormat = &Format{"lua", []string{"l"}, var INIFormat = &Format{"ini", []string{"i"}, func() Encoder { return NewINIEncoder() }, - func() Decoder { return NewINIDecoder() }, + func() Decoder { return NewINIDecoder(ConfiguredINIPreferences) }, } var Formats = []*Format{ diff --git a/pkg/yqlib/ini.go b/pkg/yqlib/ini.go index ba8bbaf2..d5bf419d 100644 --- a/pkg/yqlib/ini.go +++ b/pkg/yqlib/ini.go @@ -1,18 +1,21 @@ package yqlib type INIPreferences struct { - ColorsEnabled bool + ColorsEnabled bool + PreserveSurroundedQuote bool } func NewDefaultINIPreferences() INIPreferences { return INIPreferences{ - ColorsEnabled: false, + ColorsEnabled: false, + PreserveSurroundedQuote: false, } } func (p *INIPreferences) Copy() INIPreferences { return INIPreferences{ - ColorsEnabled: p.ColorsEnabled, + ColorsEnabled: p.ColorsEnabled, + PreserveSurroundedQuote: p.PreserveSurroundedQuote, } } diff --git a/pkg/yqlib/ini_test.go b/pkg/yqlib/ini_test.go index d73a46d1..e3ad2ef1 100644 --- a/pkg/yqlib/ini_test.go +++ b/pkg/yqlib/ini_test.go @@ -22,6 +22,16 @@ const expectedSimpleINIYaml = `section: key: value ` +const quotedINIInput = `[section] +color_theme = "Default" +theme_background = "False" +` + +const expectedQuotedINIOutput = `[section] +color_theme = "Default" +theme_background = "False" +` + var iniScenarios = []formatScenario{ { description: "Parse INI: simple", @@ -49,6 +59,22 @@ var iniScenarios = []formatScenario{ }, } +// iniPreserveQuotesPrefs returns INIPreferences with PreserveSurroundedQuote enabled. +func iniPreserveQuotesPrefs() INIPreferences { + prefs := NewDefaultINIPreferences() + prefs.PreserveSurroundedQuote = true + return prefs +} + +var iniPreserveQuotesScenarios = []formatScenario{ + { + description: "Roundtrip INI: preserve quotes", + input: quotedINIInput, + expected: expectedQuotedINIOutput, + scenarioType: "roundtrip", + }, +} + func documentRoundtripINIScenario(w *bufio.Writer, s formatScenario) { writeOrPanic(w, fmt.Sprintf("## %v\n", s.description)) @@ -70,7 +96,7 @@ func documentRoundtripINIScenario(w *bufio.Writer, s formatScenario) { } writeOrPanic(w, "will output\n") - writeOrPanic(w, fmt.Sprintf("```ini\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(), NewINIEncoder()))) + writeOrPanic(w, fmt.Sprintf("```ini\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewINIEncoder()))) } func documentDecodeINIScenario(w *bufio.Writer, s formatScenario) { @@ -94,7 +120,7 @@ func documentDecodeINIScenario(w *bufio.Writer, s formatScenario) { } writeOrPanic(w, "will output\n") - writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)))) + writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewYamlEncoder(ConfiguredYamlPreferences)))) } func testINIScenario(t *testing.T, s formatScenario) { @@ -102,11 +128,11 @@ func testINIScenario(t *testing.T, s formatScenario) { case "encode": test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewINIEncoder()), s.description) case "decode": - test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)), s.description) + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewYamlEncoder(ConfiguredYamlPreferences)), s.description) case "roundtrip": - test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(), NewINIEncoder()), s.description) + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewINIEncoder()), s.description) case "decode-error": - result, err := processFormatScenario(s, NewINIDecoder(), NewINIEncoder()) + result, err := processFormatScenario(s, NewINIDecoder(NewDefaultINIPreferences()), NewINIEncoder()) if err == nil { t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result) } else { @@ -185,3 +211,19 @@ func TestINIScenarios(t *testing.T) { } documentScenarios(t, "usage", "convert", genericScenarios, documentINIScenario) } + +func testINIPreserveQuotesScenario(t *testing.T, s formatScenario) { + prefs := iniPreserveQuotesPrefs() + switch s.scenarioType { + case "roundtrip": + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(prefs), NewINIEncoder()), s.description) + default: + panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType)) + } +} + +func TestINIPreserveQuotesScenarios(t *testing.T) { + for _, tt := range iniPreserveQuotesScenarios { + testINIPreserveQuotesScenario(t, tt) + } +} diff --git a/pkg/yqlib/no_ini.go b/pkg/yqlib/no_ini.go index df89b97c..b4540e32 100644 --- a/pkg/yqlib/no_ini.go +++ b/pkg/yqlib/no_ini.go @@ -2,7 +2,7 @@ package yqlib -func NewINIDecoder() Decoder { +func NewINIDecoder(prefs INIPreferences) Decoder { return nil }