From 3f90e4aee403062b4bb2d1690bb677af45ef6b2f Mon Sep 17 00:00:00 2001 From: Gabriel Cipriano Date: Fri, 27 Oct 2023 08:19:43 -0300 Subject: [PATCH] enhancement: Expand with default values (#285) * dx: getOr return redability * feat: expand with defaults * fix: rm unnecessary guard * docs: update readme - expand with envdefault * chore: rm readme extra spaces --------- Co-authored-by: Gabriel F Cipriano --- README.md | 29 ++++++++++++++++++++++++++++- env.go | 22 ++++++++++++++++++++-- env_test.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 16acbc6..4270c2c 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,34 @@ type config struct { } ``` -This also works with `envDefault`. +This also works with `envDefault`: +```go +import ( + "fmt" + "github.com/caarlos0/env/v9" +) + +type config struct { + Host string `env:"HOST" envDefault:"localhost"` + Port int `env:"PORT" envDefault:"3000"` + Address string `env:"ADDRESS,expand" envDefault:"$HOST:${PORT}"` +} + +func main() { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + fmt.Printf("%+v\n", cfg) +} +``` + +results in this: + +```sh +$ PORT=8080 go run main.go +{Host:localhost Port:8080 Address:localhost:8080} +``` ## Not Empty fields diff --git a/env.go b/env.go index 01a5455..5c9d806 100644 --- a/env.go +++ b/env.go @@ -122,6 +122,17 @@ type Options struct { // Custom parse functions for different types. FuncMap map[reflect.Type]ParserFunc + + // Used internally. maps the env variable key to its resolved string value. (for env var expansion) + rawEnvVars map[string]string +} + +func (opts *Options) getRawEnv(s string) string { + val := opts.rawEnvVars[s] + if val == "" { + return opts.Environment[s] + } + return val } func defaultOptions() Options { @@ -129,6 +140,7 @@ func defaultOptions() Options { TagName: "env", Environment: toMap(os.Environ()), FuncMap: defaultTypeParsers(), + rawEnvVars: make(map[string]string), } } @@ -143,6 +155,9 @@ func customOptions(opt Options) Options { if opt.FuncMap == nil { opt.FuncMap = map[reflect.Type]ParserFunc{} } + if opt.rawEnvVars == nil { + opt.rawEnvVars = defOpts.rawEnvVars + } for k, v := range defOpts.FuncMap { if _, exists := opt.FuncMap[k]; !exists { opt.FuncMap[k] = v @@ -160,6 +175,7 @@ func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options { Prefix: opts.Prefix + field.Tag.Get("envPrefix"), UseFieldNameByDefault: opts.UseFieldNameByDefault, FuncMap: opts.FuncMap, + rawEnvVars: opts.rawEnvVars, } } @@ -350,9 +366,11 @@ func get(fieldParams FieldParams, opts Options) (val string, err error) { val, exists, isDefault = getOr(fieldParams.Key, fieldParams.DefaultValue, fieldParams.HasDefaultValue, opts.Environment) if fieldParams.Expand { - val = os.ExpandEnv(val) + val = os.Expand(val, opts.getRawEnv) } + opts.rawEnvVars[fieldParams.OwnKey] = val + if fieldParams.Unset { defer os.Unsetenv(fieldParams.Key) } @@ -392,7 +410,7 @@ func getFromFile(filename string) (value string, err error) { return string(b), err } -func getOr(key, defaultValue string, defExists bool, envs map[string]string) (string, bool, bool) { +func getOr(key, defaultValue string, defExists bool, envs map[string]string) (val string, exists bool, isDefault bool) { value, exists := envs[key] switch { case (!exists || key == "") && defExists: diff --git a/env_test.go b/env_test.go index e491008..084bc29 100644 --- a/env_test.go +++ b/env_test.go @@ -965,6 +965,38 @@ func TestParseExpandOption(t *testing.T) { isEqual(t, "def1", cfg.Default) } +func TestParseExpandWithDefaultOption(t *testing.T) { + type config struct { + Host string `env:"HOST" envDefault:"localhost"` + Port int `env:"PORT,expand" envDefault:"3000"` + OtherPort int `env:"OTHER_PORT" envDefault:"4000"` + CompoundDefault string `env:"HOST_PORT,expand" envDefault:"${HOST}:${PORT}"` + SimpleDefault string `env:"DEFAULT,expand" envDefault:"def1"` + MixedDefault string `env:"MIXED_DEFAULT,expand" envDefault:"$USER@${HOST}:${OTHER_PORT}"` + OverrideDefault string `env:"OVERRIDE_DEFAULT,expand" envDefault:"$THIS_SHOULD_NOT_BE_USED"` + NoDefault string `env:"NO_DEFAULT,expand"` + } + + t.Setenv("OTHER_PORT", "5000") + t.Setenv("USER", "jhon") + t.Setenv("THIS_IS_USED", "this is used instead") + t.Setenv("OVERRIDE_DEFAULT", "msg: ${THIS_IS_USED}") + t.Setenv("NO_DEFAULT", "$PORT:$OTHER_PORT") + + cfg := config{} + err := Parse(&cfg) + + isNoErr(t, err) + isEqual(t, "localhost", cfg.Host) + isEqual(t, 3000, cfg.Port) + isEqual(t, 5000, cfg.OtherPort) + isEqual(t, "localhost:3000", cfg.CompoundDefault) + isEqual(t, "def1", cfg.SimpleDefault) + isEqual(t, "jhon@localhost:5000", cfg.MixedDefault) + isEqual(t, "msg: this is used instead", cfg.OverrideDefault) + isEqual(t, "3000:5000", cfg.NoDefault) +} + func TestParseUnsetRequireOptions(t *testing.T) { type config struct { Password string `env:"PASSWORD,unset,required"`