Skip to content

Commit 3f90e4a

Browse files
gabrielciprianoGabriel F Cipriano
and
Gabriel F Cipriano
authoredOct 27, 2023
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 <gabriel.cipriano@farme.com.br>
1 parent 13ac655 commit 3f90e4a

File tree

3 files changed

+80
-3
lines changed

3 files changed

+80
-3
lines changed
 

‎README.md

+28-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,34 @@ type config struct {
180180
}
181181
```
182182

183-
This also works with `envDefault`.
183+
This also works with `envDefault`:
184+
```go
185+
import (
186+
"fmt"
187+
"github.com/caarlos0/env/v9"
188+
)
189+
190+
type config struct {
191+
Host string `env:"HOST" envDefault:"localhost"`
192+
Port int `env:"PORT" envDefault:"3000"`
193+
Address string `env:"ADDRESS,expand" envDefault:"$HOST:${PORT}"`
194+
}
195+
196+
func main() {
197+
cfg := config{}
198+
if err := env.Parse(&cfg); err != nil {
199+
fmt.Printf("%+v\n", err)
200+
}
201+
fmt.Printf("%+v\n", cfg)
202+
}
203+
```
204+
205+
results in this:
206+
207+
```sh
208+
$ PORT=8080 go run main.go
209+
{Host:localhost Port:8080 Address:localhost:8080}
210+
```
184211

185212
## Not Empty fields
186213

‎env.go

+20-2
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,25 @@ type Options struct {
122122

123123
// Custom parse functions for different types.
124124
FuncMap map[reflect.Type]ParserFunc
125+
126+
// Used internally. maps the env variable key to its resolved string value. (for env var expansion)
127+
rawEnvVars map[string]string
128+
}
129+
130+
func (opts *Options) getRawEnv(s string) string {
131+
val := opts.rawEnvVars[s]
132+
if val == "" {
133+
return opts.Environment[s]
134+
}
135+
return val
125136
}
126137

127138
func defaultOptions() Options {
128139
return Options{
129140
TagName: "env",
130141
Environment: toMap(os.Environ()),
131142
FuncMap: defaultTypeParsers(),
143+
rawEnvVars: make(map[string]string),
132144
}
133145
}
134146

@@ -143,6 +155,9 @@ func customOptions(opt Options) Options {
143155
if opt.FuncMap == nil {
144156
opt.FuncMap = map[reflect.Type]ParserFunc{}
145157
}
158+
if opt.rawEnvVars == nil {
159+
opt.rawEnvVars = defOpts.rawEnvVars
160+
}
146161
for k, v := range defOpts.FuncMap {
147162
if _, exists := opt.FuncMap[k]; !exists {
148163
opt.FuncMap[k] = v
@@ -160,6 +175,7 @@ func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options {
160175
Prefix: opts.Prefix + field.Tag.Get("envPrefix"),
161176
UseFieldNameByDefault: opts.UseFieldNameByDefault,
162177
FuncMap: opts.FuncMap,
178+
rawEnvVars: opts.rawEnvVars,
163179
}
164180
}
165181

@@ -350,9 +366,11 @@ func get(fieldParams FieldParams, opts Options) (val string, err error) {
350366
val, exists, isDefault = getOr(fieldParams.Key, fieldParams.DefaultValue, fieldParams.HasDefaultValue, opts.Environment)
351367

352368
if fieldParams.Expand {
353-
val = os.ExpandEnv(val)
369+
val = os.Expand(val, opts.getRawEnv)
354370
}
355371

372+
opts.rawEnvVars[fieldParams.OwnKey] = val
373+
356374
if fieldParams.Unset {
357375
defer os.Unsetenv(fieldParams.Key)
358376
}
@@ -392,7 +410,7 @@ func getFromFile(filename string) (value string, err error) {
392410
return string(b), err
393411
}
394412

395-
func getOr(key, defaultValue string, defExists bool, envs map[string]string) (string, bool, bool) {
413+
func getOr(key, defaultValue string, defExists bool, envs map[string]string) (val string, exists bool, isDefault bool) {
396414
value, exists := envs[key]
397415
switch {
398416
case (!exists || key == "") && defExists:

‎env_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,38 @@ func TestParseExpandOption(t *testing.T) {
965965
isEqual(t, "def1", cfg.Default)
966966
}
967967

968+
func TestParseExpandWithDefaultOption(t *testing.T) {
969+
type config struct {
970+
Host string `env:"HOST" envDefault:"localhost"`
971+
Port int `env:"PORT,expand" envDefault:"3000"`
972+
OtherPort int `env:"OTHER_PORT" envDefault:"4000"`
973+
CompoundDefault string `env:"HOST_PORT,expand" envDefault:"${HOST}:${PORT}"`
974+
SimpleDefault string `env:"DEFAULT,expand" envDefault:"def1"`
975+
MixedDefault string `env:"MIXED_DEFAULT,expand" envDefault:"$USER@${HOST}:${OTHER_PORT}"`
976+
OverrideDefault string `env:"OVERRIDE_DEFAULT,expand" envDefault:"$THIS_SHOULD_NOT_BE_USED"`
977+
NoDefault string `env:"NO_DEFAULT,expand"`
978+
}
979+
980+
t.Setenv("OTHER_PORT", "5000")
981+
t.Setenv("USER", "jhon")
982+
t.Setenv("THIS_IS_USED", "this is used instead")
983+
t.Setenv("OVERRIDE_DEFAULT", "msg: ${THIS_IS_USED}")
984+
t.Setenv("NO_DEFAULT", "$PORT:$OTHER_PORT")
985+
986+
cfg := config{}
987+
err := Parse(&cfg)
988+
989+
isNoErr(t, err)
990+
isEqual(t, "localhost", cfg.Host)
991+
isEqual(t, 3000, cfg.Port)
992+
isEqual(t, 5000, cfg.OtherPort)
993+
isEqual(t, "localhost:3000", cfg.CompoundDefault)
994+
isEqual(t, "def1", cfg.SimpleDefault)
995+
isEqual(t, "jhon@localhost:5000", cfg.MixedDefault)
996+
isEqual(t, "msg: this is used instead", cfg.OverrideDefault)
997+
isEqual(t, "3000:5000", cfg.NoDefault)
998+
}
999+
9681000
func TestParseUnsetRequireOptions(t *testing.T) {
9691001
type config struct {
9701002
Password string `env:"PASSWORD,unset,required"`

0 commit comments

Comments
 (0)