From 35eb824a59412116476b45992d37cd85851fb88e Mon Sep 17 00:00:00 2001 From: Team Humaki Date: Thu, 17 Sep 2026 02:09:19 -0700 Subject: [PATCH 1/2] key: keep empty shadows when PreserveEmptyShadows is set ValueWithShadows dropped empty assignments, which breaks systemd-style resets. Opt in with LoadOptions.PreserveEmptyShadows. Fixes #366 --- ini.go | 4 ++++ key.go | 11 ++++++----- key_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/ini.go b/ini.go index 99e7f86..552e333 100644 --- a/ini.go +++ b/ini.go @@ -125,6 +125,10 @@ type LoadOptions struct { AllowNonUniqueSections bool // AllowDuplicateShadowValues indicates whether values for shadowed keys should be deduplicated. AllowDuplicateShadowValues bool + // PreserveEmptyShadows indicates whether ValueWithShadows should keep empty + // assignments. Empty values are otherwise omitted (the historical default). + // systemd unit files use an empty assignment to reset prior values. + PreserveEmptyShadows bool } // DebugFunc is the type of function called to log parse events. diff --git a/key.go b/key.go index 800d04a..8008868 100644 --- a/key.go +++ b/key.go @@ -110,22 +110,23 @@ func (k *Key) Value() string { return k.value } -// ValueWithShadows returns raw values of key and its shadows if any. Shadow -// keys with empty values are ignored from the returned list. +// ValueWithShadows returns raw values of key and its shadows if any. +// Empty values are omitted unless LoadOptions.PreserveEmptyShadows is set. func (k *Key) ValueWithShadows() []string { + keepEmpty := k.s != nil && k.s.f != nil && k.s.f.options.PreserveEmptyShadows if len(k.shadows) == 0 { - if k.value == "" { + if k.value == "" && !keepEmpty { return []string{} } return []string{k.value} } vals := make([]string, 0, len(k.shadows)+1) - if k.value != "" { + if k.value != "" || keepEmpty { vals = append(vals, k.value) } for _, s := range k.shadows { - if s.value != "" { + if s.value != "" || keepEmpty { vals = append(vals, s.value) } } diff --git a/key_test.go b/key_test.go index e57a7bd..9f51711 100644 --- a/key_test.go +++ b/key_test.go @@ -66,6 +66,20 @@ func TestKey_AddShadow(t *testing.T) { }) }) + t.Run("preserve empty shadow values", func(t *testing.T) { + f := Empty(LoadOptions{ + AllowShadows: true, + PreserveEmptyShadows: true, + }) + require.NotNil(t, f) + + k, err := f.Section("").NewKey("Environment", "FOO=bar") + require.NoError(t, err) + require.NotNil(t, k) + assert.NoError(t, k.AddShadow("")) + assert.Equal(t, []string{"FOO=bar", ""}, k.ValueWithShadows()) + }) + t.Run("allow duplicate shadowed values", func(t *testing.T) { f := Empty(LoadOptions{ AllowShadows: true, @@ -546,6 +560,31 @@ keyName = value2 require.NotNil(t, k) assert.Equal(t, []string{"value1", "value2"}, k.ValueWithShadows()) }) + + t.Run("preserve empty assignments from file", func(t *testing.T) { + f, err := LoadSources(LoadOptions{ + AllowShadows: true, + PreserveEmptyShadows: true, + }, []byte(` +[Service] +Environment=FOO=bar +Environment= +`)) + require.NoError(t, err) + require.NotNil(t, f) + assert.Equal(t, []string{"FOO=bar", ""}, f.Section("Service").Key("Environment").ValueWithShadows()) + }) + + t.Run("default still drops empty assignments from file", func(t *testing.T) { + f, err := ShadowLoad([]byte(` +[Service] +Environment=FOO=bar +Environment= +`)) + require.NoError(t, err) + require.NotNil(t, f) + assert.Equal(t, []string{"FOO=bar"}, f.Section("Service").Key("Environment").ValueWithShadows()) + }) } func TestKey_StringsWithShadows(t *testing.T) { From db44e95e5974d0464c55ac68143e0fdbe4bdd25a Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Wed, 11 Nov 2026 20:20:26 +0000 Subject: [PATCH 2/2] Keep every empty shadow value, and rename the option to PreserveEmptyShadowValues An empty value kept by the option is a reset, so it isn't deduplicated. The name matches AllowDuplicateShadowValues and the v2 option. --- ini.go | 9 +++++---- key.go | 10 ++++++---- key_test.go | 21 +++++++++++++++++---- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/ini.go b/ini.go index 552e333..5eb9d5a 100644 --- a/ini.go +++ b/ini.go @@ -125,10 +125,11 @@ type LoadOptions struct { AllowNonUniqueSections bool // AllowDuplicateShadowValues indicates whether values for shadowed keys should be deduplicated. AllowDuplicateShadowValues bool - // PreserveEmptyShadows indicates whether ValueWithShadows should keep empty - // assignments. Empty values are otherwise omitted (the historical default). - // systemd unit files use an empty assignment to reset prior values. - PreserveEmptyShadows bool + // PreserveEmptyShadowValues indicates whether ValueWithShadows should keep + // empty values, and not deduplicate them, as systemd unit files use an + // empty assignment to reset prior values. Empty values are otherwise + // omitted. + PreserveEmptyShadowValues bool } // DebugFunc is the type of function called to log parse events. diff --git a/key.go b/key.go index 8008868..a188817 100644 --- a/key.go +++ b/key.go @@ -54,8 +54,10 @@ func (k *Key) addShadow(val string) error { return errors.New("cannot add shadow to auto-increment or boolean key") } - if !k.s.f.options.AllowDuplicateShadowValues { - // Deduplicate shadows based on their values. + // Deduplicate shadows based on their values. A kept empty value is a + // reset, so each one counts. + keepEmpty := val == "" && k.s.f.options.PreserveEmptyShadowValues + if !k.s.f.options.AllowDuplicateShadowValues && !keepEmpty { if k.value == val { return nil } @@ -111,9 +113,9 @@ func (k *Key) Value() string { } // ValueWithShadows returns raw values of key and its shadows if any. -// Empty values are omitted unless LoadOptions.PreserveEmptyShadows is set. +// Empty values are omitted unless LoadOptions.PreserveEmptyShadowValues is set. func (k *Key) ValueWithShadows() []string { - keepEmpty := k.s != nil && k.s.f != nil && k.s.f.options.PreserveEmptyShadows + keepEmpty := k.s.f.options.PreserveEmptyShadowValues if len(k.shadows) == 0 { if k.value == "" && !keepEmpty { return []string{} diff --git a/key_test.go b/key_test.go index 9f51711..b6fa584 100644 --- a/key_test.go +++ b/key_test.go @@ -68,8 +68,8 @@ func TestKey_AddShadow(t *testing.T) { t.Run("preserve empty shadow values", func(t *testing.T) { f := Empty(LoadOptions{ - AllowShadows: true, - PreserveEmptyShadows: true, + AllowShadows: true, + PreserveEmptyShadowValues: true, }) require.NotNil(t, f) @@ -563,8 +563,8 @@ keyName = value2 t.Run("preserve empty assignments from file", func(t *testing.T) { f, err := LoadSources(LoadOptions{ - AllowShadows: true, - PreserveEmptyShadows: true, + AllowShadows: true, + PreserveEmptyShadowValues: true, }, []byte(` [Service] Environment=FOO=bar @@ -575,6 +575,19 @@ Environment= assert.Equal(t, []string{"FOO=bar", ""}, f.Section("Service").Key("Environment").ValueWithShadows()) }) + t.Run("preserve every empty assignment on write", func(t *testing.T) { + const data = "[Service]\nEnvironment = A\nEnvironment = \nEnvironment = B\nEnvironment = \n" + opts := LoadOptions{AllowShadows: true, PreserveEmptyShadowValues: true} + f, err := LoadSources(opts, []byte(data)) + require.NoError(t, err) + assert.Equal(t, []string{"A", "", "B", ""}, f.Section("Service").Key("Environment").ValueWithShadows()) + + var buf bytes.Buffer + _, err = f.WriteTo(&buf) + require.NoError(t, err) + assert.Equal(t, data, buf.String()) + }) + t.Run("default still drops empty assignments from file", func(t *testing.T) { f, err := ShadowLoad([]byte(` [Service]