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"`