Skip to content

Commit

Permalink
enhancement: Expand with default values (caarlos0#285)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
2 people authored and BorzdeG committed Aug 24, 2024
1 parent 28fc8a5 commit ee7ace9
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 3 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 20 additions & 2 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,25 @@ 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 {
return Options{
TagName: "env",
Environment: toMap(os.Environ()),
FuncMap: defaultTypeParsers(),
rawEnvVars: make(map[string]string),
}
}

Expand All @@ -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
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down

0 comments on commit ee7ace9

Please sign in to comment.