Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancement: Expand with default values #285

Merged
merged 5 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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