Repository: gohugoio/hugo (language: go)

Issue:
Not possible to set outputformats (and possibly others) in a YAML env var
And there's some test cases to be added here:

* `HUGO_OUTPUTS=home: ['json']` with and without existing outputs config
* `HUGO_OUTPUTS_HOME=json`




Discussion / hints:

Fix patch:
diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go
index a40a02372b1..6b8a4568cfd 100644
--- a/config/allconfig/alldecoders.go
+++ b/config/allconfig/alldecoders.go
@@ -150,7 +150,7 @@ var allDecoderSetups = map[string]decodeWeight{
 		key: "outputs",
 		decode: func(d decodeWeight, p decodeConfig) error {
 			defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
-			m := p.p.GetStringMap("outputs")
+			m := maps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
 			p.c.Outputs = make(map[string][]string)
 			for k, v := range m {
 				s := types.ToStringSlicePreserveString(v)
diff --git a/config/allconfig/load.go b/config/allconfig/load.go
index b9bb38aaf40..8551039a0ee 100644
--- a/config/allconfig/load.go
+++ b/config/allconfig/load.go
@@ -293,11 +293,19 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
 			} else {
 				l.cfg.Set(env.Key, val)
 			}
-		} else if nestedKey != "" {
-			owner[nestedKey] = env.Value
 		} else {
-			// The container does not exist yet.
-			l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value)
+			if nestedKey != "" {
+				owner[nestedKey] = env.Value
+			} else {
+				var val any = env.Value
+				if _, ok := allDecoderSetups[env.Key]; ok {
+					// A map.
+					val, err = metadecoders.Default.UnmarshalStringTo(env.Value, map[string]interface{}{})
+				}
+				if err == nil {
+					l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), val)
+				}
+			}
 		}
 	}
 
diff --git a/parser/metadecoders/decoder.go b/parser/metadecoders/decoder.go
index 93eb32e475a..40b3a336ced 100644
--- a/parser/metadecoders/decoder.go
+++ b/parser/metadecoders/decoder.go
@@ -23,6 +23,7 @@ import (
 	"strings"
 
 	"github.com/gohugoio/hugo/common/herrors"
+	"github.com/gohugoio/hugo/common/maps"
 	"github.com/niklasfasching/go-org/org"
 
 	xml "github.com/clbanning/mxj/v2"
@@ -90,7 +91,7 @@ func (d Decoder) UnmarshalStringTo(data string, typ any) (any, error) {
 	switch typ.(type) {
 	case string:
 		return data, nil
-	case map[string]any:
+	case map[string]any, maps.Params:
 		format := d.FormatFromContentString(data)
 		return d.UnmarshalToMap([]byte(data), format)
 	case []any:


Test patch:
diff --git a/hugolib/config_test.go b/hugolib/config_test.go
index 100144ca146..9720522ad0e 100644
--- a/hugolib/config_test.go
+++ b/hugolib/config_test.go
@@ -1419,3 +1419,118 @@ Home.
 	b.Assert(len(b.H.Sites), qt.Equals, 1)
 
 }
+
+func TestLoadConfigYamlEnvVar(t *testing.T) {
+
+	defaultEnv := []string{`HUGO_OUTPUTS=home: ['json']`}
+
+	runVariant := func(t testing.TB, files string, env []string) *IntegrationTestBuilder {
+		if env == nil {
+			env = defaultEnv
+		}
+
+		b := NewIntegrationTestBuilder(
+			IntegrationTestConfig{
+				T:           t,
+				TxtarString: files,
+				Environ:     env,
+				BuildCfg:    BuildCfg{SkipRender: true},
+			},
+		).Build()
+
+		outputs := b.H.Configs.Base.Outputs
+		if env == nil {
+			home := outputs["home"]
+			b.Assert(home, qt.Not(qt.IsNil))
+			b.Assert(home, qt.DeepEquals, []string{"json"})
+		}
+
+		return b
+
+	}
+
+	t.Run("with empty slice", func(t *testing.T) {
+		t.Parallel()
+
+		files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
+[outputs]
+home = ["html"]
+
+		`
+		b := runVariant(t, files, []string{`HUGO_OUTPUTS=section: []`})
+		outputs := b.H.Configs.Base.Outputs
+		b.Assert(outputs, qt.DeepEquals, map[string][]string{
+			"home":     {"html"},
+			"page":     {"html"},
+			"rss":      {"rss"},
+			"section":  nil,
+			"taxonomy": {"html", "rss"},
+			"term":     {"html", "rss"},
+		})
+
+	})
+
+	t.Run("with existing outputs", func(t *testing.T) {
+		t.Parallel()
+
+		files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
+[outputs]
+home = ["html"]
+
+		`
+
+		runVariant(t, files, nil)
+
+	})
+
+	{
+		t.Run("with existing outputs direct", func(t *testing.T) {
+			t.Parallel()
+
+			files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
+[outputs]
+home = ["html"]
+
+		`
+			runVariant(t, files, []string{"HUGO_OUTPUTS_HOME=json"})
+
+		})
+	}
+
+	t.Run("without existing outputs", func(t *testing.T) {
+		t.Parallel()
+
+		files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
+		
+		`
+
+		runVariant(t, files, nil)
+
+	})
+
+	t.Run("without existing outputs direct", func(t *testing.T) {
+		t.Parallel()
+
+		files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
+		`
+
+		runVariant(t, files, []string{"HUGO_OUTPUTS_HOME=json"})
+
+	})
+
+}