Fix setting config from env with complex (e.g. YAML) strings - hugo - [fork] hugo port for 9front
 (HTM) git clone git@git.drkhsh.at/hugo.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit c406fd3a0e0efa17f69095ca6317ba1036fc8964
 (DIR) parent 286821e360e13b3a174854914c9cedd437bdd25e
 (HTM) Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Sun, 16 Jul 2023 10:42:13 +0200
       
       Fix setting config from env with complex (e.g. YAML) strings
       
       So you can do
       
       ```
       HUGO_OUTPUTS="home: [rss]"  hugo
       ```
       
       And similar.
       
       Fixes #11249
       
       Diffstat:
         M config/allconfig/alldecoders.go     |       2 +-
         M config/allconfig/load.go            |      16 ++++++++++++----
         M hugolib/config_test.go              |     115 +++++++++++++++++++++++++++++++
         M parser/metadecoders/decoder.go      |       3 ++-
       
       4 files changed, 130 insertions(+), 6 deletions(-)
       ---
 (DIR) diff --git 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)
 (DIR) diff --git 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)
       +                                }
       +                        }
                        }
                }
        
 (DIR) diff --git 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"})
       +
       +        })
       +
       +}
 (DIR) diff --git 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: