Allow setting the delimiter used for setting config via OS env, e.g. HUGO_
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 24 Nov 2020 13:11:42 +0000 (14:11 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 25 Nov 2020 19:34:34 +0000 (20:34 +0100)
Fixes #7829

common/maps/params.go
docs/content/en/getting-started/configuration.md
hugolib/config.go
hugolib/config_test.go

index 1f0856598353748656574ba63192284dd1826d80..5e973051b4592c6ab9997271778670a30747f1d0 100644 (file)
@@ -84,7 +84,7 @@ func GetNestedParam(keyStr, separator string, candidates ...Params) (interface{}
 }
 
 func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) {
-       keySegments := strings.Split(strings.ToLower(keyStr), separator)
+       keySegments := strings.Split(keyStr, separator)
        if len(keySegments) == 0 {
                return nil, "", nil, nil
        }
index fda7e2327a55e56a2d5c1db6b47a8345091ddac5..d12ecdf3be0c9ea20d05f5fcc9b64c3b175f6de5 100644 (file)
@@ -427,6 +427,8 @@ Names must be prefixed with `HUGO_` and the configuration key must be set in upp
 To set config params, prefix the name with `HUGO_PARAMS_`
 {{% /note %}}
 
+{{< new-in "0.79.0" >}} If you are using snake_cased variable names, the above will not work, so since Hugo 0.79.0 Hugo determines the delimiter to use by the first character after `HUGO`. This allows you to define environment variables on the form `HUGOxPARAMSxAPI_KEY=abcdefgh`, using any [allowed](https://stackoverflow.com/questions/2821043/allowed-characters-in-linux-environment-variable-names#:~:text=So%20names%20may%20contain%20any,not%20begin%20with%20a%20digit.) delimiter.
+
 {{< todo >}}
 Test and document setting params via JSON env var.
 {{< /todo >}}
index 72b51272b5964fec4d88b6ea70656202744365d8..9acb7d701e2224bdc02374e5c149bc6c196d1cb9 100644 (file)
@@ -18,6 +18,8 @@ import (
        "path/filepath"
        "strings"
 
+       "github.com/gohugoio/hugo/common/types"
+
        "github.com/gobwas/glob"
        hglob "github.com/gohugoio/hugo/hugofs/glob"
 
@@ -166,45 +168,59 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid
                }
        }
 
+       const delim = "__env__delim"
+
        // Apply environment overrides
        if len(d.Environ) > 0 {
-               // Extract all that start with the HUGO_ prefix
-               const hugoEnvPrefix = "HUGO_"
-               var hugoEnv []string
+               // Extract all that start with the HUGO prefix.
+               // The delimiter is the following rune, usually "_".
+               const hugoEnvPrefix = "HUGO"
+               var hugoEnv []types.KeyValueStr
                for _, v := range d.Environ {
                        key, val := config.SplitEnvVar(v)
                        if strings.HasPrefix(key, hugoEnvPrefix) {
-                               hugoEnv = append(hugoEnv, strings.ToLower(strings.TrimPrefix(key, hugoEnvPrefix)), val)
+                               delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
+                               if len(delimiterAndKey) < 2 {
+                                       continue
+                               }
+                               // Allow delimiters to be case sensitive.
+                               // It turns out there isn't that many allowed special
+                               // chars in environment variables when used in Bash and similar,
+                               // so variables on the form HUGOxPARAMSxFOO=bar is one option.
+                               key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim)
+                               key = strings.ToLower(key)
+                               hugoEnv = append(hugoEnv, types.KeyValueStr{
+                                       Key:   key,
+                                       Value: val,
+                               })
+
                        }
                }
 
-               if len(hugoEnv) > 0 {
-                       for i := 0; i < len(hugoEnv); i += 2 {
-                               key, valStr := strings.ToLower(hugoEnv[i]), hugoEnv[i+1]
+               for _, env := range hugoEnv {
+                       existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, v.Get)
+                       if err != nil {
+                               return v, configFiles, err
+                       }
 
-                               existing, nestedKey, owner, err := maps.GetNestedParamFn(key, "_", v.Get)
+                       if existing != nil {
+                               val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing)
                                if err != nil {
-                                       return v, configFiles, err
+                                       continue
                                }
 
-                               if existing != nil {
-                                       val, err := metadecoders.Default.UnmarshalStringTo(valStr, existing)
-                                       if err != nil {
-                                               continue
-                                       }
-
-                                       if owner != nil {
-                                               owner[nestedKey] = val
-                                       } else {
-                                               v.Set(key, val)
-                                       }
-                               } else if nestedKey != "" {
-                                       owner[nestedKey] = valStr
+                               if owner != nil {
+                                       owner[nestedKey] = val
                                } else {
-                                       v.Set(key, valStr)
+                                       v.Set(env.Key, val)
                                }
+                       } else if nestedKey != "" {
+                               owner[nestedKey] = env.Value
+                       } else {
+                               v.Set(env.Key, env.Value)
                        }
                }
+
        }
 
        // We made this a Glob pattern in Hugo 0.75, we don't need both.
index cb9c1d8f62729175573592bbf5aefacefdc1585c..e8dce331defdf7e86f4b848033db46b448c43abc 100644 (file)
@@ -492,6 +492,11 @@ intSlice = [5,7,9]
 floatSlice = [3.14, 5.19]
 stringSlice = ["a", "b"]
 
+[params]
+[params.api_config]
+api_key="default_key"
+another_key="default another_key"
+
 [imaging]
 anchor = "smart"
 quality = 75 
@@ -508,6 +513,10 @@ quality = 75
                "HUGO_STRINGSLICE", `["c", "d"]`,
                "HUGO_INTSLICE", `[5, 8, 9]`,
                "HUGO_FLOATSLICE", `[5.32]`,
+               // https://github.com/gohugoio/hugo/issues/7829
+               "HUGOxPARAMSxAPI_CONFIGxAPI_KEY", "new_key",
+               // Delimiters are case sensitive.
+               "HUGOxPARAMSxAPI_CONFIGXANOTHER_KEY", "another_key",
        )
 
        b.Build(BuildCfg{})
@@ -523,5 +532,7 @@ quality = 75
        c.Assert(cfg.Get("stringSlice"), qt.DeepEquals, []interface{}{"c", "d"})
        c.Assert(cfg.Get("floatSlice"), qt.DeepEquals, []interface{}{5.32})
        c.Assert(cfg.Get("intSlice"), qt.DeepEquals, []interface{}{5, 8, 9})
+       c.Assert(cfg.Get("params.api_config.api_key"), qt.Equals, "new_key")
+       c.Assert(cfg.Get("params.api_config.another_key"), qt.Equals, "default another_key")
 
 }