Skip to content
Open
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
32 changes: 25 additions & 7 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,24 @@ type Options struct {
rawEnvVars map[string]string
}

func (opts *Options) getRawEnv(s string) string {
val := opts.rawEnvVars[s]
if val == "" {
val = opts.Environment[s]
}
return os.Expand(val, opts.getRawEnv)
// expand replaces ${var} or $var in val, expanding the values it refers to
// as well. visiting holds the variables being expanded, so a reference back
// to one of them (e.g. A=${A}) expands to an empty string instead of
// recursing until the stack overflows.
func (opts *Options) expand(val string, visiting map[string]bool) string {
return os.Expand(val, func(s string) string {
if visiting[s] {
return ""
}
visiting[s] = true
defer delete(visiting, s)

raw := opts.rawEnvVars[s]
if raw == "" {
raw = opts.Environment[s]
}
return opts.expand(raw, visiting)
})
}

func defaultOptions() Options {
Expand Down Expand Up @@ -601,7 +613,13 @@ func get(fieldParams FieldParams, opts Options) (val string, err error) {
)

if fieldParams.Expand {
val = os.Expand(val, opts.getRawEnv)
visiting := map[string]bool{}
// A reference to this field's own key is a cycle, unless an earlier
// field with the same key already cached a value to read.
if opts.rawEnvVars[fieldParams.Key] == "" {
visiting[fieldParams.Key] = true
}
val = opts.expand(val, visiting)
}

opts.rawEnvVars[fieldParams.Key] = val
Expand Down
37 changes: 37 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,43 @@ func TestParseExpandWithDefaultOption(t *testing.T) {
isEqual(t, "3000:5000", cfg.NoDefault)
}

func TestParseExpandCyclicReference(t *testing.T) {
type config struct {
Self string `env:"SELF,expand"`
Indirect string `env:"INDIRECT,expand"`
Twice string `env:"TWICE,expand"`
}

t.Setenv("SELF", "a-${SELF}")
t.Setenv("INDIRECT", "b-${LOOP_A}")
t.Setenv("LOOP_A", "${LOOP_B}")
t.Setenv("LOOP_B", "${LOOP_A}")
t.Setenv("TWICE", "${ONE}${ONE}")
t.Setenv("ONE", "1")

cfg := config{}
err := Parse(&cfg)

isNoErr(t, err)
isEqual(t, "a-", cfg.Self)
isEqual(t, "b-", cfg.Indirect)
isEqual(t, "11", cfg.Twice)
}

func TestParseExpandReadsEarlierFieldWithSameKey(t *testing.T) {
type config struct {
RawPort string `env:"PORT" envDefault:"3000"`
Port int `env:"PORT,expand" envDefault:"${PORT}"`
}

var cfg config
err := ParseWithOptions(&cfg, Options{Environment: map[string]string{}})

isNoErr(t, err)
isEqual(t, "3000", cfg.RawPort)
isEqual(t, 3000, cfg.Port)
}

func TestParseUnsetRequireOptions(t *testing.T) {
type config struct {
Password string `env:"PASSWORD,unset,required"`
Expand Down