├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── envdecode.go ├── envdecode_test.go ├── example └── main.go └── go.mod /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | example/example 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.9.x 4 | - master 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Joe Shaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # envdecode [![Travis-CI](https://travis-ci.org/joeshaw/envdecode.svg)](https://travis-ci.org/joeshaw/envdecode) [![GoDoc](https://godoc.org/github.com/joeshaw/envdecode?status.svg)](https://godoc.org/github.com/joeshaw/envdecode) 2 | 3 | `envdecode` is a Go package for populating structs from environment 4 | variables. 5 | 6 | `envdecode` uses struct tags to map environment variables to fields, 7 | allowing you you use any names you want for environment variables. 8 | `envdecode` will recurse into nested structs, including pointers to 9 | nested structs, but it will not allocate new pointers to structs. 10 | 11 | ## API 12 | 13 | Full API docs are available on 14 | [godoc.org](https://godoc.org/github.com/joeshaw/envdecode). 15 | 16 | Define a struct with `env` struct tags: 17 | 18 | ```go 19 | type Config struct { 20 | Hostname string `env:"SERVER_HOSTNAME,default=localhost"` 21 | Port uint16 `env:"SERVER_PORT,default=8080"` 22 | 23 | AWS struct { 24 | ID string `env:"AWS_ACCESS_KEY_ID"` 25 | Secret string `env:"AWS_SECRET_ACCESS_KEY,required"` 26 | SnsTopics []string `env:"AWS_SNS_TOPICS"` 27 | } 28 | 29 | Timeout time.Duration `env:"TIMEOUT,default=1m,strict"` 30 | } 31 | ``` 32 | 33 | Fields *must be exported* (i.e. begin with a capital letter) in order 34 | for `envdecode` to work with them. An error will be returned if a 35 | struct with no exported fields is decoded (including one that contains 36 | no `env` tags at all). 37 | Default values may be provided by appending ",default=value" to the 38 | struct tag. Required values may be marked by appending ",required" to the 39 | struct tag. Strict values may be marked by appending ",strict" which will 40 | return an error on Decode if there is an error while parsing. 41 | 42 | Then call `envdecode.Decode`: 43 | 44 | ```go 45 | var cfg Config 46 | err := envdecode.Decode(&cfg) 47 | ``` 48 | 49 | If you want all fields to act `strict`, you may use `envdecode.StrictDecode`: 50 | 51 | ```go 52 | var cfg Config 53 | err := envdecode.StrictDecode(&cfg) 54 | ``` 55 | 56 | All parse errors will fail fast and return an error in this mode. 57 | 58 | ## Supported types 59 | 60 | * Structs (and pointer to structs) 61 | * Slices of below defined types, separated by semicolon 62 | * `bool` 63 | * `float32`, `float64` 64 | * `int`, `int8`, `int16`, `int32`, `int64` 65 | * `uint`, `uint8`, `uint16`, `uint32`, `uint64` 66 | * `string` 67 | * `time.Duration`, using the [`time.ParseDuration()` format](http://golang.org/pkg/time/#ParseDuration) 68 | * `*url.URL`, using [`url.Parse()`](https://godoc.org/net/url#Parse) 69 | * Types those implement a `Decoder` interface 70 | 71 | ## Custom `Decoder` 72 | 73 | If you want a field to be decoded with custom behavior, you may implement the interface `Decoder` for the filed type. 74 | 75 | ```go 76 | type Config struct { 77 | IPAddr IP `env:"IP_ADDR"` 78 | } 79 | 80 | type IP net.IP 81 | 82 | // Decode implements the interface `envdecode.Decoder` 83 | func (i *IP) Decode(repl string) error { 84 | *i = net.ParseIP(repl) 85 | return nil 86 | } 87 | ``` 88 | 89 | `Decoder` is the interface implemented by an object that can decode an environment variable string representation of itself. 90 | -------------------------------------------------------------------------------- /envdecode.go: -------------------------------------------------------------------------------- 1 | // Package envdecode is a package for populating structs from environment 2 | // variables, using struct tags. 3 | package envdecode 4 | 5 | import ( 6 | "encoding" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net/url" 11 | "os" 12 | "reflect" 13 | "sort" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // ErrInvalidTarget indicates that the target value passed to 20 | // Decode is invalid. Target must be a non-nil pointer to a struct. 21 | var ErrInvalidTarget = errors.New("target must be non-nil pointer to struct that has at least one exported field with a valid env tag.") 22 | var ErrNoTargetFieldsAreSet = errors.New("none of the target fields were set from environment variables") 23 | 24 | // FailureFunc is called when an error is encountered during a MustDecode 25 | // operation. It prints the error and terminates the process. 26 | // 27 | // This variable can be assigned to another function of the user-programmer's 28 | // design, allowing for graceful recovery of the problem, such as loading 29 | // from a backup configuration file. 30 | var FailureFunc = func(err error) { 31 | log.Fatalf("envdecode: an error was encountered while decoding: %v\n", err) 32 | } 33 | 34 | // Decoder is the interface implemented by an object that can decode an 35 | // environment variable string representation of itself. 36 | type Decoder interface { 37 | Decode(string) error 38 | } 39 | 40 | // Decode environment variables into the provided target. The target 41 | // must be a non-nil pointer to a struct. Fields in the struct must 42 | // be exported, and tagged with an "env" struct tag with a value 43 | // containing the name of the environment variable. An error is 44 | // returned if there are no exported members tagged. 45 | // 46 | // Default values may be provided by appending ",default=value" to the 47 | // struct tag. Required values may be marked by appending ",required" 48 | // to the struct tag. It is an error to provide both "default" and 49 | // "required". Strict values may be marked by appending ",strict" which 50 | // will return an error on Decode if there is an error while parsing. 51 | // If everything must be strict, consider using StrictDecode instead. 52 | // 53 | // All primitive types are supported, including bool, floating point, 54 | // signed and unsigned integers, and string. Boolean and numeric 55 | // types are decoded using the standard strconv Parse functions for 56 | // those types. Structs and pointers to structs are decoded 57 | // recursively. time.Duration is supported via the 58 | // time.ParseDuration() function and *url.URL is supported via the 59 | // url.Parse() function. Slices are supported for all above mentioned 60 | // primitive types. Semicolon is used as delimiter in environment variables. 61 | func Decode(target interface{}) error { 62 | nFields, err := decode(target, false) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // if we didn't do anything - the user probably did something 68 | // wrong like leave all fields unexported. 69 | if nFields == 0 { 70 | return ErrNoTargetFieldsAreSet 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // StrictDecode is similar to Decode except all fields will have an implicit 77 | // ",strict" on all fields. 78 | func StrictDecode(target interface{}) error { 79 | nFields, err := decode(target, true) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | // if we didn't do anything - the user probably did something 85 | // wrong like leave all fields unexported. 86 | if nFields == 0 { 87 | return ErrInvalidTarget 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func decode(target interface{}, strict bool) (int, error) { 94 | s := reflect.ValueOf(target) 95 | if s.Kind() != reflect.Ptr || s.IsNil() { 96 | return 0, ErrInvalidTarget 97 | } 98 | 99 | s = s.Elem() 100 | if s.Kind() != reflect.Struct { 101 | return 0, ErrInvalidTarget 102 | } 103 | 104 | t := s.Type() 105 | setFieldCount := 0 106 | for i := 0; i < s.NumField(); i++ { 107 | // Localize the umbrella `strict` value to the specific field. 108 | strict := strict 109 | 110 | f := s.Field(i) 111 | 112 | switch f.Kind() { 113 | case reflect.Ptr: 114 | if f.Elem().Kind() != reflect.Struct { 115 | break 116 | } 117 | 118 | f = f.Elem() 119 | fallthrough 120 | 121 | case reflect.Struct: 122 | if !f.Addr().CanInterface() { 123 | continue 124 | } 125 | 126 | ss := f.Addr().Interface() 127 | _, custom := ss.(Decoder) 128 | if custom { 129 | break 130 | } 131 | 132 | n, err := decode(ss, strict) 133 | if err != nil { 134 | return 0, err 135 | } 136 | setFieldCount += n 137 | } 138 | 139 | if !f.CanSet() { 140 | continue 141 | } 142 | 143 | tag := t.Field(i).Tag.Get("env") 144 | if tag == "" { 145 | continue 146 | } 147 | 148 | parts := strings.Split(tag, ",") 149 | env := os.Getenv(parts[0]) 150 | 151 | required := false 152 | hasDefault := false 153 | defaultValue := "" 154 | 155 | for _, o := range parts[1:] { 156 | if !required { 157 | required = strings.HasPrefix(o, "required") 158 | } 159 | if strings.HasPrefix(o, "default=") { 160 | hasDefault = true 161 | defaultValue = o[8:] 162 | } 163 | if !strict { 164 | strict = strings.HasPrefix(o, "strict") 165 | } 166 | } 167 | 168 | if required && hasDefault { 169 | panic(`envdecode: "default" and "required" may not be specified in the same annotation`) 170 | } 171 | if env == "" && required { 172 | return 0, fmt.Errorf("the environment variable \"%s\" is missing", parts[0]) 173 | } 174 | if env == "" { 175 | env = defaultValue 176 | } 177 | if env == "" { 178 | continue 179 | } 180 | 181 | setFieldCount++ 182 | 183 | unmarshaler, implementsUnmarshaler := f.Addr().Interface().(encoding.TextUnmarshaler) 184 | decoder, implmentsDecoder := f.Addr().Interface().(Decoder) 185 | if implmentsDecoder { 186 | if err := decoder.Decode(env); err != nil { 187 | return 0, err 188 | } 189 | } else if implementsUnmarshaler { 190 | if err := unmarshaler.UnmarshalText([]byte(env)); err != nil { 191 | return 0, err 192 | } 193 | } else if f.Kind() == reflect.Slice { 194 | decodeSlice(&f, env) 195 | } else { 196 | if err := decodePrimitiveType(&f, env); err != nil && strict { 197 | return 0, err 198 | } 199 | } 200 | } 201 | 202 | return setFieldCount, nil 203 | } 204 | 205 | func decodeSlice(f *reflect.Value, env string) { 206 | parts := strings.Split(env, ";") 207 | 208 | values := parts[:0] 209 | for _, x := range parts { 210 | if x != "" { 211 | values = append(values, strings.TrimSpace(x)) 212 | } 213 | } 214 | 215 | valuesCount := len(values) 216 | slice := reflect.MakeSlice(f.Type(), valuesCount, valuesCount) 217 | if valuesCount > 0 { 218 | for i := 0; i < valuesCount; i++ { 219 | e := slice.Index(i) 220 | decodePrimitiveType(&e, values[i]) 221 | } 222 | } 223 | 224 | f.Set(slice) 225 | } 226 | 227 | func decodePrimitiveType(f *reflect.Value, env string) error { 228 | switch f.Kind() { 229 | case reflect.Bool: 230 | v, err := strconv.ParseBool(env) 231 | if err != nil { 232 | return err 233 | } 234 | f.SetBool(v) 235 | 236 | case reflect.Float32, reflect.Float64: 237 | bits := f.Type().Bits() 238 | v, err := strconv.ParseFloat(env, bits) 239 | if err != nil { 240 | return err 241 | } 242 | f.SetFloat(v) 243 | 244 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 245 | if t := f.Type(); t.PkgPath() == "time" && t.Name() == "Duration" { 246 | v, err := time.ParseDuration(env) 247 | if err != nil { 248 | return err 249 | } 250 | f.SetInt(int64(v)) 251 | } else { 252 | bits := f.Type().Bits() 253 | v, err := strconv.ParseInt(env, 0, bits) 254 | if err != nil { 255 | return err 256 | } 257 | f.SetInt(v) 258 | } 259 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 260 | bits := f.Type().Bits() 261 | v, err := strconv.ParseUint(env, 0, bits) 262 | if err != nil { 263 | return err 264 | } 265 | f.SetUint(v) 266 | 267 | case reflect.String: 268 | f.SetString(env) 269 | 270 | case reflect.Ptr: 271 | if t := f.Type().Elem(); t.Kind() == reflect.Struct && t.PkgPath() == "net/url" && t.Name() == "URL" { 272 | v, err := url.Parse(env) 273 | if err != nil { 274 | return err 275 | } 276 | f.Set(reflect.ValueOf(v)) 277 | } 278 | } 279 | return nil 280 | } 281 | 282 | // MustDecode calls Decode and terminates the process if any errors 283 | // are encountered. 284 | func MustDecode(target interface{}) { 285 | err := Decode(target) 286 | if err != nil { 287 | FailureFunc(err) 288 | } 289 | } 290 | 291 | // MustStrictDecode calls StrictDecode and terminates the process if any errors 292 | // are encountered. 293 | func MustStrictDecode(target interface{}) { 294 | err := StrictDecode(target) 295 | if err != nil { 296 | FailureFunc(err) 297 | } 298 | } 299 | 300 | //// Configuration info for Export 301 | 302 | type ConfigInfo struct { 303 | Field string 304 | EnvVar string 305 | Value string 306 | DefaultValue string 307 | HasDefault bool 308 | Required bool 309 | UsesEnv bool 310 | } 311 | 312 | type ConfigInfoSlice []*ConfigInfo 313 | 314 | func (c ConfigInfoSlice) Less(i, j int) bool { 315 | return c[i].EnvVar < c[j].EnvVar 316 | } 317 | func (c ConfigInfoSlice) Len() int { 318 | return len(c) 319 | } 320 | func (c ConfigInfoSlice) Swap(i, j int) { 321 | c[i], c[j] = c[j], c[i] 322 | } 323 | 324 | // Returns a list of final configuration metadata sorted by envvar name 325 | func Export(target interface{}) ([]*ConfigInfo, error) { 326 | s := reflect.ValueOf(target) 327 | if s.Kind() != reflect.Ptr || s.IsNil() { 328 | return nil, ErrInvalidTarget 329 | } 330 | 331 | cfg := []*ConfigInfo{} 332 | 333 | s = s.Elem() 334 | if s.Kind() != reflect.Struct { 335 | return nil, ErrInvalidTarget 336 | } 337 | 338 | t := s.Type() 339 | for i := 0; i < s.NumField(); i++ { 340 | f := s.Field(i) 341 | fName := t.Field(i).Name 342 | 343 | fElem := f 344 | if f.Kind() == reflect.Ptr { 345 | fElem = f.Elem() 346 | } 347 | if fElem.Kind() == reflect.Struct { 348 | ss := fElem.Addr().Interface() 349 | subCfg, err := Export(ss) 350 | if err != ErrInvalidTarget { 351 | f = fElem 352 | for _, v := range subCfg { 353 | v.Field = fmt.Sprintf("%s.%s", fName, v.Field) 354 | cfg = append(cfg, v) 355 | } 356 | } 357 | } 358 | 359 | tag := t.Field(i).Tag.Get("env") 360 | if tag == "" { 361 | continue 362 | } 363 | 364 | parts := strings.Split(tag, ",") 365 | 366 | ci := &ConfigInfo{ 367 | Field: fName, 368 | EnvVar: parts[0], 369 | UsesEnv: os.Getenv(parts[0]) != "", 370 | } 371 | 372 | for _, o := range parts[1:] { 373 | if strings.HasPrefix(o, "default=") { 374 | ci.HasDefault = true 375 | ci.DefaultValue = o[8:] 376 | } else if strings.HasPrefix(o, "required") { 377 | ci.Required = true 378 | } 379 | } 380 | 381 | if f.Kind() == reflect.Ptr && f.IsNil() { 382 | ci.Value = "" 383 | } else if stringer, ok := f.Interface().(fmt.Stringer); ok { 384 | ci.Value = stringer.String() 385 | } else { 386 | switch f.Kind() { 387 | case reflect.Bool: 388 | ci.Value = strconv.FormatBool(f.Bool()) 389 | 390 | case reflect.Float32, reflect.Float64: 391 | bits := f.Type().Bits() 392 | ci.Value = strconv.FormatFloat(f.Float(), 'f', -1, bits) 393 | 394 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 395 | ci.Value = strconv.FormatInt(f.Int(), 10) 396 | 397 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 398 | ci.Value = strconv.FormatUint(f.Uint(), 10) 399 | 400 | case reflect.String: 401 | ci.Value = f.String() 402 | 403 | case reflect.Slice: 404 | ci.Value = fmt.Sprintf("%v", f.Interface()) 405 | 406 | default: 407 | // Unable to determine string format for value 408 | return nil, ErrInvalidTarget 409 | } 410 | } 411 | 412 | cfg = append(cfg, ci) 413 | } 414 | 415 | // No configuration tags found, assume invalid input 416 | if len(cfg) == 0 { 417 | return nil, ErrInvalidTarget 418 | } 419 | 420 | sort.Sort(ConfigInfoSlice(cfg)) 421 | 422 | return cfg, nil 423 | } 424 | -------------------------------------------------------------------------------- /envdecode_test.go: -------------------------------------------------------------------------------- 1 | package envdecode 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math" 7 | "net/url" 8 | "os" 9 | "reflect" 10 | "sort" 11 | "strconv" 12 | "sync" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | type nested struct { 18 | String string `env:"TEST_STRING"` 19 | } 20 | 21 | type testConfig struct { 22 | String string `env:"TEST_STRING"` 23 | Int64 int64 `env:"TEST_INT64"` 24 | Uint16 uint16 `env:"TEST_UINT16"` 25 | Float64 float64 `env:"TEST_FLOAT64"` 26 | Bool bool `env:"TEST_BOOL"` 27 | Duration time.Duration `env:"TEST_DURATION"` 28 | URL *url.URL `env:"TEST_URL"` 29 | 30 | StringSlice []string `env:"TEST_STRING_SLICE"` 31 | Int64Slice []int64 `env:"TEST_INT64_SLICE"` 32 | Uint16Slice []uint16 `env:"TEST_UINT16_SLICE"` 33 | Float64Slice []float64 `env:"TEST_FLOAT64_SLICE"` 34 | BoolSlice []bool `env:"TEST_BOOL_SLICE"` 35 | DurationSlice []time.Duration `env:"TEST_DURATION_SLICE"` 36 | URLSlice []*url.URL `env:"TEST_URL_SLICE"` 37 | 38 | UnsetString string `env:"TEST_UNSET_STRING"` 39 | UnsetInt64 int64 `env:"TEST_UNSET_INT64"` 40 | UnsetDuration time.Duration `env:"TEST_UNSET_DURATION"` 41 | UnsetURL *url.URL `env:"TEST_UNSET_URL"` 42 | UnsetSlice []string `env:"TEST_UNSET_SLICE"` 43 | 44 | InvalidInt64 int64 `env:"TEST_INVALID_INT64"` 45 | 46 | UnusedField string 47 | unexportedField string 48 | 49 | IgnoredPtr *bool `env:"TEST_BOOL"` 50 | 51 | Nested nested 52 | NestedPtr *nested 53 | 54 | DecoderStruct decoderStruct `env:"TEST_DECODER_STRUCT"` 55 | DecoderStructPtr *decoderStruct `env:"TEST_DECODER_STRUCT_PTR"` 56 | 57 | DecoderString decoderString `env:"TEST_DECODER_STRING"` 58 | 59 | UnmarshalerNumber unmarshalerNumber `env:"TEST_UNMARSHALER_NUMBER"` 60 | 61 | DefaultInt int `env:"TEST_UNSET,asdf=asdf,default=1234"` 62 | DefaultSliceInt []int `env:"TEST_UNSET,asdf=asdf,default=1;2;3"` 63 | DefaultDuration time.Duration `env:"TEST_UNSET,asdf=asdf,default=24h"` 64 | DefaultURL *url.URL `env:"TEST_UNSET,default=http://example.com"` 65 | 66 | cantInterfaceField sync.Mutex 67 | } 68 | 69 | type testConfigNoSet struct { 70 | Some string `env:"TEST_THIS_ENV_WILL_NOT_BE_SET"` 71 | } 72 | 73 | type testConfigRequired struct { 74 | Required string `env:"TEST_REQUIRED,required"` 75 | } 76 | 77 | type testConfigRequiredDefault struct { 78 | RequiredDefault string `env:"TEST_REQUIRED_DEFAULT,required,default=test"` 79 | } 80 | 81 | type testNoExportedFields struct { 82 | aString string `env:"TEST_STRING"` 83 | anInt64 int64 `env:"TEST_INT64"` 84 | aUint16 uint16 `env:"TEST_UINT16"` 85 | aFloat64 float64 `env:"TEST_FLOAT64"` 86 | aBool bool `env:"TEST_BOOL"` 87 | } 88 | 89 | type testNoTags struct { 90 | String string 91 | } 92 | 93 | type decoderStruct struct { 94 | String string 95 | } 96 | 97 | func (d *decoderStruct) Decode(env string) error { 98 | return json.Unmarshal([]byte(env), &d) 99 | } 100 | 101 | type decoderString string 102 | 103 | func (d *decoderString) Decode(env string) error { 104 | r, l := []rune(env), len(env) 105 | 106 | for i := 0; i < l/2; i++ { 107 | r[i], r[l-1-i] = r[l-1-i], r[i] 108 | } 109 | 110 | *d = decoderString(r) 111 | return nil 112 | } 113 | 114 | type unmarshalerNumber uint8 115 | 116 | func (o *unmarshalerNumber) UnmarshalText(raw []byte) error { 117 | n, err := strconv.ParseUint(string(raw), 8, 8) // parse text as octal number 118 | if err != nil { 119 | return err 120 | } 121 | *o = unmarshalerNumber(n) 122 | return nil 123 | } 124 | 125 | func TestDecode(t *testing.T) { 126 | int64Val := int64(-(1 << 50)) 127 | int64AsString := fmt.Sprintf("%d", int64Val) 128 | piAsString := fmt.Sprintf("%.48f", math.Pi) 129 | 130 | os.Setenv("TEST_STRING", "foo") 131 | os.Setenv("TEST_INT64", int64AsString) 132 | os.Setenv("TEST_UINT16", "60000") 133 | os.Setenv("TEST_FLOAT64", piAsString) 134 | os.Setenv("TEST_BOOL", "true") 135 | os.Setenv("TEST_DURATION", "10m") 136 | os.Setenv("TEST_URL", "https://example.com") 137 | os.Setenv("TEST_INVALID_INT64", "asdf") 138 | os.Setenv("TEST_STRING_SLICE", "foo;bar") 139 | os.Setenv("TEST_INT64_SLICE", int64AsString+";"+int64AsString) 140 | os.Setenv("TEST_UINT16_SLICE", "60000;50000") 141 | os.Setenv("TEST_FLOAT64_SLICE", piAsString+";"+piAsString) 142 | os.Setenv("TEST_BOOL_SLICE", "true; false; true") 143 | os.Setenv("TEST_DURATION_SLICE", "10m; 20m") 144 | os.Setenv("TEST_URL_SLICE", "https://example.com") 145 | os.Setenv("TEST_DECODER_STRUCT", "{\"string\":\"foo\"}") 146 | os.Setenv("TEST_DECODER_STRUCT_PTR", "{\"string\":\"foo\"}") 147 | os.Setenv("TEST_DECODER_STRING", "oof") 148 | os.Setenv("TEST_UNMARSHALER_NUMBER", "07") 149 | 150 | var tc testConfig 151 | tc.NestedPtr = &nested{} 152 | tc.DecoderStructPtr = &decoderStruct{} 153 | 154 | err := Decode(&tc) 155 | if err != nil { 156 | t.Fatal(err) 157 | } 158 | 159 | if tc.String != "foo" { 160 | t.Fatalf(`Expected "foo", got "%s"`, tc.String) 161 | } 162 | 163 | if tc.Int64 != -(1 << 50) { 164 | t.Fatalf("Expected %d, got %d", -(1 << 50), tc.Int64) 165 | } 166 | 167 | if tc.Uint16 != 60000 { 168 | t.Fatalf("Expected 60000, got %d", tc.Uint16) 169 | } 170 | 171 | if tc.Float64 != math.Pi { 172 | t.Fatalf("Expected %.48f, got %.48f", math.Pi, tc.Float64) 173 | } 174 | 175 | if !tc.Bool { 176 | t.Fatal("Expected true, got false") 177 | } 178 | 179 | duration, _ := time.ParseDuration("10m") 180 | if tc.Duration != duration { 181 | t.Fatalf("Expected %d, got %d", duration, tc.Duration) 182 | } 183 | 184 | if tc.URL == nil { 185 | t.Fatalf("Expected https://example.com, got nil") 186 | } else if tc.URL.String() != "https://example.com" { 187 | t.Fatalf("Expected https://example.com, got %s", tc.URL.String()) 188 | } 189 | 190 | expectedStringSlice := []string{"foo", "bar"} 191 | if !reflect.DeepEqual(tc.StringSlice, expectedStringSlice) { 192 | t.Fatalf("Expected %s, got %s", expectedStringSlice, tc.StringSlice) 193 | } 194 | 195 | expectedInt64Slice := []int64{int64Val, int64Val} 196 | if !reflect.DeepEqual(tc.Int64Slice, expectedInt64Slice) { 197 | t.Fatalf("Expected %#v, got %#v", expectedInt64Slice, tc.Int64Slice) 198 | } 199 | 200 | expectedUint16Slice := []uint16{60000, 50000} 201 | if !reflect.DeepEqual(tc.Uint16Slice, expectedUint16Slice) { 202 | t.Fatalf("Expected %#v, got %#v", expectedUint16Slice, tc.Uint16Slice) 203 | } 204 | 205 | expectedFloat64Slice := []float64{math.Pi, math.Pi} 206 | if !reflect.DeepEqual(tc.Float64Slice, expectedFloat64Slice) { 207 | t.Fatalf("Expected %#v, got %#v", expectedFloat64Slice, tc.Float64Slice) 208 | } 209 | 210 | expectedBoolSlice := []bool{true, false, true} 211 | if !reflect.DeepEqual(tc.BoolSlice, expectedBoolSlice) { 212 | t.Fatalf("Expected %#v, got %#v", expectedBoolSlice, tc.BoolSlice) 213 | } 214 | 215 | duration2, _ := time.ParseDuration("20m") 216 | expectedDurationSlice := []time.Duration{duration, duration2} 217 | if !reflect.DeepEqual(tc.DurationSlice, expectedDurationSlice) { 218 | t.Fatalf("Expected %s, got %s", expectedDurationSlice, tc.DurationSlice) 219 | } 220 | 221 | urlVal, _ := url.Parse("https://example.com") 222 | expectedUrlSlice := []*url.URL{urlVal} 223 | if !reflect.DeepEqual(tc.URLSlice, expectedUrlSlice) { 224 | t.Fatalf("Expected %s, got %s", expectedUrlSlice, tc.URLSlice) 225 | } 226 | 227 | if tc.UnsetString != "" { 228 | t.Fatal("Got non-empty string unexpectedly") 229 | } 230 | 231 | if tc.UnsetInt64 != 0 { 232 | t.Fatal("Got non-zero int unexpectedly") 233 | } 234 | 235 | if tc.UnsetDuration != time.Duration(0) { 236 | t.Fatal("Got non-zero time.Duration unexpectedly") 237 | } 238 | 239 | if tc.UnsetURL != nil { 240 | t.Fatal("Got non-zero *url.URL unexpectedly") 241 | } 242 | 243 | if len(tc.UnsetSlice) > 0 { 244 | t.Fatal("Got not-empty string slice unexpectedly") 245 | } 246 | 247 | if tc.InvalidInt64 != 0 { 248 | t.Fatal("Got non-zero int unexpectedly") 249 | } 250 | 251 | if tc.UnusedField != "" { 252 | t.Fatal("Expected empty field") 253 | } 254 | 255 | if tc.unexportedField != "" { 256 | t.Fatal("Expected empty field") 257 | } 258 | 259 | if tc.IgnoredPtr != nil { 260 | t.Fatal("Expected nil pointer") 261 | } 262 | 263 | if tc.Nested.String != "foo" { 264 | t.Fatalf(`Expected "foo", got "%s"`, tc.Nested.String) 265 | } 266 | 267 | if tc.NestedPtr.String != "foo" { 268 | t.Fatalf(`Expected "foo", got "%s"`, tc.NestedPtr.String) 269 | } 270 | 271 | if tc.DefaultInt != 1234 { 272 | t.Fatalf("Expected 1234, got %d", tc.DefaultInt) 273 | } 274 | 275 | expectedDefaultSlice := []int{1, 2, 3} 276 | if !reflect.DeepEqual(tc.DefaultSliceInt, expectedDefaultSlice) { 277 | t.Fatalf("Expected %d, got %d", expectedDefaultSlice, tc.DefaultSliceInt) 278 | } 279 | 280 | defaultDuration, _ := time.ParseDuration("24h") 281 | if tc.DefaultDuration != defaultDuration { 282 | t.Fatalf("Expected %d, got %d", defaultDuration, tc.DefaultInt) 283 | } 284 | 285 | if tc.DefaultURL.String() != "http://example.com" { 286 | t.Fatalf("Expected http://example.com, got %s", tc.DefaultURL.String()) 287 | } 288 | 289 | if tc.DecoderStruct.String != "foo" { 290 | t.Fatalf("Expected foo, got %s", tc.DecoderStruct.String) 291 | } 292 | 293 | if tc.DecoderStructPtr.String != "foo" { 294 | t.Fatalf("Expected foo, got %s", tc.DecoderStructPtr.String) 295 | } 296 | 297 | if tc.DecoderString != "foo" { 298 | t.Fatalf("Expected foo, got %s", tc.DecoderString) 299 | } 300 | 301 | if tc.UnmarshalerNumber != 07 { 302 | t.Fatalf("Expected 07, got %04o", tc.UnmarshalerNumber) 303 | } 304 | 305 | os.Setenv("TEST_REQUIRED", "required") 306 | var tcr testConfigRequired 307 | 308 | err = Decode(&tcr) 309 | if err != nil { 310 | t.Fatal(err) 311 | } 312 | 313 | if tcr.Required != "required" { 314 | t.Fatalf("Expected \"required\", got %s", tcr.Required) 315 | } 316 | 317 | _, err = Export(&tcr) 318 | if err != nil { 319 | t.Fatal(err) 320 | } 321 | } 322 | 323 | func TestDecodeErrors(t *testing.T) { 324 | var b bool 325 | err := Decode(&b) 326 | if err != ErrInvalidTarget { 327 | t.Fatal("Should have gotten an error decoding into a bool") 328 | } 329 | 330 | var tc testConfig 331 | err = Decode(tc) 332 | if err != ErrInvalidTarget { 333 | t.Fatal("Should have gotten an error decoding into a non-pointer") 334 | } 335 | 336 | var tcp *testConfig 337 | err = Decode(tcp) 338 | if err != ErrInvalidTarget { 339 | t.Fatal("Should have gotten an error decoding to a nil pointer") 340 | } 341 | 342 | var tnt testNoTags 343 | err = Decode(&tnt) 344 | if err != ErrNoTargetFieldsAreSet { 345 | t.Fatal("Should have gotten an error decoding a struct with no tags") 346 | } 347 | 348 | var tcni testNoExportedFields 349 | err = Decode(&tcni) 350 | if err != ErrNoTargetFieldsAreSet { 351 | t.Fatal("Should have gotten an error decoding a struct with no unexported fields") 352 | } 353 | 354 | var tcr testConfigRequired 355 | os.Clearenv() 356 | err = Decode(&tcr) 357 | if err == nil { 358 | t.Fatal("An error was expected but recieved:", err) 359 | } 360 | 361 | var tcns testConfigNoSet 362 | err = Decode(&tcns) 363 | if err != ErrNoTargetFieldsAreSet { 364 | t.Fatal("Should have gotten an error decoding when no env variables are set") 365 | } 366 | 367 | missing := false 368 | FailureFunc = func(err error) { 369 | missing = true 370 | } 371 | MustDecode(&tcr) 372 | if !missing { 373 | t.Fatal("The FailureFunc should have been called but it was not") 374 | } 375 | 376 | var tcrd testConfigRequiredDefault 377 | defer func() { 378 | if r := recover(); r != nil { 379 | } 380 | }() 381 | err = Decode(&tcrd) 382 | t.Fatal("This should not have been reached. A panic should have occured.") 383 | } 384 | 385 | func TestOnlyNested(t *testing.T) { 386 | os.Setenv("TEST_STRING", "foo") 387 | 388 | // No env vars in the outer level are ok, as long as they're 389 | // in the inner struct. 390 | var o struct { 391 | Inner nested 392 | } 393 | if err := Decode(&o); err != nil { 394 | t.Fatalf("Expected no error, got %s", err) 395 | } 396 | 397 | // No env vars in the inner levels are ok, as long as they're 398 | // in the outer struct. 399 | var o2 struct { 400 | Inner noConfig 401 | X string `env:"TEST_STRING"` 402 | } 403 | if err := Decode(&o2); err != nil { 404 | t.Fatalf("Expected no error, got %s", err) 405 | } 406 | 407 | // No env vars in either outer or inner levels should result 408 | // in error 409 | var o3 struct { 410 | Inner noConfig 411 | } 412 | if err := Decode(&o3); err != ErrNoTargetFieldsAreSet { 413 | t.Fatalf("Expected ErrInvalidTarget, got %s", err) 414 | } 415 | } 416 | 417 | func ExampleDecode() { 418 | type Example struct { 419 | // A string field, without any default 420 | String string `env:"EXAMPLE_STRING"` 421 | 422 | // A uint16 field, with a default value of 100 423 | Uint16 uint16 `env:"EXAMPLE_UINT16,default=100"` 424 | } 425 | 426 | os.Setenv("EXAMPLE_STRING", "an example!") 427 | 428 | var e Example 429 | err := Decode(&e) 430 | if err != nil { 431 | panic(err) 432 | } 433 | 434 | // If TEST_STRING is set, e.String will contain its value 435 | fmt.Println(e.String) 436 | 437 | // If TEST_UINT16 is set, e.Uint16 will contain its value. 438 | // Otherwise, it will contain the default value, 100. 439 | fmt.Println(e.Uint16) 440 | 441 | // Output: 442 | // an example! 443 | // 100 444 | } 445 | 446 | //// Export tests 447 | 448 | type testConfigExport struct { 449 | String string `env:"TEST_STRING"` 450 | Int64 int64 `env:"TEST_INT64"` 451 | Uint16 uint16 `env:"TEST_UINT16"` 452 | Float64 float64 `env:"TEST_FLOAT64"` 453 | Bool bool `env:"TEST_BOOL"` 454 | Duration time.Duration `env:"TEST_DURATION"` 455 | URL *url.URL `env:"TEST_URL"` 456 | 457 | StringSlice []string `env:"TEST_STRING_SLICE"` 458 | 459 | UnsetString string `env:"TEST_UNSET_STRING"` 460 | UnsetInt64 int64 `env:"TEST_UNSET_INT64"` 461 | UnsetDuration time.Duration `env:"TEST_UNSET_DURATION"` 462 | UnsetURL *url.URL `env:"TEST_UNSET_URL"` 463 | 464 | UnusedField string 465 | unexportedField string 466 | 467 | IgnoredPtr *bool `env:"TEST_IGNORED_POINTER"` 468 | 469 | Nested nestedConfigExport 470 | NestedPtr *nestedConfigExportPointer 471 | NestedPtrUnset *nestedConfigExportPointer 472 | 473 | NestedTwice nestedTwiceConfig 474 | 475 | NoConfig noConfig 476 | NoConfigPtr *noConfig 477 | NoConfigPtrSet *noConfig 478 | 479 | RequiredInt int `env:"TEST_REQUIRED_INT,required"` 480 | 481 | DefaultBool bool `env:"TEST_DEFAULT_BOOL,default=true"` 482 | DefaultInt int `env:"TEST_DEFAULT_INT,default=1234"` 483 | DefaultDuration time.Duration `env:"TEST_DEFAULT_DURATION,default=24h"` 484 | DefaultURL *url.URL `env:"TEST_DEFAULT_URL,default=http://example.com"` 485 | DefaultIntSet int `env:"TEST_DEFAULT_INT_SET,default=99"` 486 | DefaultIntSlice []int `env:"TEST_DEFAULT_INT_SLICE,default=99;33"` 487 | } 488 | 489 | type nestedConfigExport struct { 490 | String string `env:"TEST_NESTED_STRING"` 491 | } 492 | 493 | type nestedConfigExportPointer struct { 494 | String string `env:"TEST_NESTED_STRING_POINTER"` 495 | } 496 | 497 | type noConfig struct { 498 | Int int 499 | } 500 | 501 | type nestedTwiceConfig struct { 502 | Nested nestedConfigInner 503 | } 504 | 505 | type nestedConfigInner struct { 506 | String string `env:"TEST_NESTED_TWICE_STRING"` 507 | } 508 | 509 | type testConfigStrict struct { 510 | InvalidInt64Strict int64 `env:"TEST_INVALID_INT64,strict,default=1"` 511 | InvalidInt64Implicit int64 `env:"TEST_INVALID_INT64_IMPLICIT,default=1"` 512 | 513 | Nested struct { 514 | InvalidInt64Strict int64 `env:"TEST_INVALID_INT64_NESTED,strict,required"` 515 | InvalidInt64Implicit int64 `env:"TEST_INVALID_INT64_NESTED_IMPLICIT,required"` 516 | } 517 | } 518 | 519 | func TestInvalidStrict(t *testing.T) { 520 | cases := []struct { 521 | decoder func(interface{}) error 522 | rootValue string 523 | nestedValue string 524 | rootValueImplicit string 525 | nestedValueImplicit string 526 | pass bool 527 | }{ 528 | {Decode, "1", "1", "1", "1", true}, 529 | {Decode, "1", "1", "1", "asdf", true}, 530 | {Decode, "1", "1", "asdf", "1", true}, 531 | {Decode, "1", "1", "asdf", "asdf", true}, 532 | {Decode, "1", "asdf", "1", "1", false}, 533 | {Decode, "asdf", "1", "1", "1", false}, 534 | {Decode, "asdf", "asdf", "1", "1", false}, 535 | {StrictDecode, "1", "1", "1", "1", true}, 536 | {StrictDecode, "asdf", "1", "1", "1", false}, 537 | {StrictDecode, "1", "asdf", "1", "1", false}, 538 | {StrictDecode, "1", "1", "asdf", "1", false}, 539 | {StrictDecode, "1", "1", "1", "asdf", false}, 540 | {StrictDecode, "asdf", "asdf", "1", "1", false}, 541 | {StrictDecode, "1", "asdf", "asdf", "1", false}, 542 | {StrictDecode, "1", "1", "asdf", "asdf", false}, 543 | {StrictDecode, "1", "asdf", "asdf", "asdf", false}, 544 | {StrictDecode, "asdf", "asdf", "asdf", "asdf", false}, 545 | } 546 | 547 | for _, test := range cases { 548 | os.Setenv("TEST_INVALID_INT64", test.rootValue) 549 | os.Setenv("TEST_INVALID_INT64_NESTED", test.nestedValue) 550 | os.Setenv("TEST_INVALID_INT64_IMPLICIT", test.rootValueImplicit) 551 | os.Setenv("TEST_INVALID_INT64_NESTED_IMPLICIT", test.nestedValueImplicit) 552 | 553 | var tc testConfigStrict 554 | if err := test.decoder(&tc); test.pass != (err == nil) { 555 | t.Fatalf("Have err=%s wanted pass=%v", err, test.pass) 556 | } 557 | } 558 | } 559 | 560 | func TestExport(t *testing.T) { 561 | testFloat64 := fmt.Sprintf("%.48f", math.Pi) 562 | testFloat64Output := strconv.FormatFloat(math.Pi, 'f', -1, 64) 563 | testInt64 := fmt.Sprintf("%d", -(1 << 50)) 564 | 565 | os.Setenv("TEST_STRING", "foo") 566 | os.Setenv("TEST_INT64", testInt64) 567 | os.Setenv("TEST_UINT16", "60000") 568 | os.Setenv("TEST_FLOAT64", testFloat64) 569 | os.Setenv("TEST_BOOL", "true") 570 | os.Setenv("TEST_DURATION", "10m") 571 | os.Setenv("TEST_URL", "https://example.com") 572 | os.Setenv("TEST_STRING_SLICE", "foo;bar") 573 | os.Setenv("TEST_NESTED_STRING", "nest_foo") 574 | os.Setenv("TEST_NESTED_STRING_POINTER", "nest_foo_ptr") 575 | os.Setenv("TEST_NESTED_TWICE_STRING", "nest_twice_foo") 576 | os.Setenv("TEST_REQUIRED_INT", "101") 577 | os.Setenv("TEST_DEFAULT_INT_SET", "102") 578 | os.Setenv("TEST_DEFAULT_INT_SLICE", "1;2;3") 579 | 580 | var tc testConfigExport 581 | tc.NestedPtr = &nestedConfigExportPointer{} 582 | tc.NoConfigPtrSet = &noConfig{} 583 | 584 | err := Decode(&tc) 585 | if err != nil { 586 | t.Fatal(err) 587 | } 588 | 589 | rc, err := Export(&tc) 590 | if err != nil { 591 | t.Fatal(err) 592 | } 593 | 594 | expected := []*ConfigInfo{ 595 | &ConfigInfo{ 596 | Field: "String", 597 | EnvVar: "TEST_STRING", 598 | Value: "foo", 599 | UsesEnv: true, 600 | }, 601 | &ConfigInfo{ 602 | Field: "Int64", 603 | EnvVar: "TEST_INT64", 604 | Value: testInt64, 605 | UsesEnv: true, 606 | }, 607 | &ConfigInfo{ 608 | Field: "Uint16", 609 | EnvVar: "TEST_UINT16", 610 | Value: "60000", 611 | UsesEnv: true, 612 | }, 613 | &ConfigInfo{ 614 | Field: "Float64", 615 | EnvVar: "TEST_FLOAT64", 616 | Value: testFloat64Output, 617 | UsesEnv: true, 618 | }, 619 | &ConfigInfo{ 620 | Field: "Bool", 621 | EnvVar: "TEST_BOOL", 622 | Value: "true", 623 | UsesEnv: true, 624 | }, 625 | &ConfigInfo{ 626 | Field: "Duration", 627 | EnvVar: "TEST_DURATION", 628 | Value: "10m0s", 629 | UsesEnv: true, 630 | }, 631 | &ConfigInfo{ 632 | Field: "URL", 633 | EnvVar: "TEST_URL", 634 | Value: "https://example.com", 635 | UsesEnv: true, 636 | }, 637 | &ConfigInfo{ 638 | Field: "StringSlice", 639 | EnvVar: "TEST_STRING_SLICE", 640 | Value: "[foo bar]", 641 | UsesEnv: true, 642 | }, 643 | 644 | &ConfigInfo{ 645 | Field: "UnsetString", 646 | EnvVar: "TEST_UNSET_STRING", 647 | Value: "", 648 | }, 649 | &ConfigInfo{ 650 | Field: "UnsetInt64", 651 | EnvVar: "TEST_UNSET_INT64", 652 | Value: "0", 653 | }, 654 | &ConfigInfo{ 655 | Field: "UnsetDuration", 656 | EnvVar: "TEST_UNSET_DURATION", 657 | Value: "0s", 658 | }, 659 | &ConfigInfo{ 660 | Field: "UnsetURL", 661 | EnvVar: "TEST_UNSET_URL", 662 | Value: "", 663 | }, 664 | 665 | &ConfigInfo{ 666 | Field: "IgnoredPtr", 667 | EnvVar: "TEST_IGNORED_POINTER", 668 | Value: "", 669 | }, 670 | 671 | &ConfigInfo{ 672 | Field: "Nested.String", 673 | EnvVar: "TEST_NESTED_STRING", 674 | Value: "nest_foo", 675 | UsesEnv: true, 676 | }, 677 | &ConfigInfo{ 678 | Field: "NestedPtr.String", 679 | EnvVar: "TEST_NESTED_STRING_POINTER", 680 | Value: "nest_foo_ptr", 681 | UsesEnv: true, 682 | }, 683 | 684 | &ConfigInfo{ 685 | Field: "NestedTwice.Nested.String", 686 | EnvVar: "TEST_NESTED_TWICE_STRING", 687 | Value: "nest_twice_foo", 688 | UsesEnv: true, 689 | }, 690 | 691 | &ConfigInfo{ 692 | Field: "RequiredInt", 693 | EnvVar: "TEST_REQUIRED_INT", 694 | Value: "101", 695 | UsesEnv: true, 696 | Required: true, 697 | }, 698 | 699 | &ConfigInfo{ 700 | Field: "DefaultBool", 701 | EnvVar: "TEST_DEFAULT_BOOL", 702 | Value: "true", 703 | DefaultValue: "true", 704 | HasDefault: true, 705 | }, 706 | &ConfigInfo{ 707 | Field: "DefaultInt", 708 | EnvVar: "TEST_DEFAULT_INT", 709 | Value: "1234", 710 | DefaultValue: "1234", 711 | HasDefault: true, 712 | }, 713 | &ConfigInfo{ 714 | Field: "DefaultDuration", 715 | EnvVar: "TEST_DEFAULT_DURATION", 716 | Value: "24h0m0s", 717 | DefaultValue: "24h", 718 | HasDefault: true, 719 | }, 720 | &ConfigInfo{ 721 | Field: "DefaultURL", 722 | EnvVar: "TEST_DEFAULT_URL", 723 | Value: "http://example.com", 724 | DefaultValue: "http://example.com", 725 | HasDefault: true, 726 | }, 727 | &ConfigInfo{ 728 | Field: "DefaultIntSet", 729 | EnvVar: "TEST_DEFAULT_INT_SET", 730 | Value: "102", 731 | DefaultValue: "99", 732 | HasDefault: true, 733 | UsesEnv: true, 734 | }, 735 | &ConfigInfo{ 736 | Field: "DefaultIntSlice", 737 | EnvVar: "TEST_DEFAULT_INT_SLICE", 738 | Value: "[1 2 3]", 739 | DefaultValue: "99;33", 740 | HasDefault: true, 741 | UsesEnv: true, 742 | }, 743 | } 744 | 745 | sort.Sort(ConfigInfoSlice(expected)) 746 | 747 | if len(rc) != len(expected) { 748 | t.Fatalf("Have %d results, expected %d", len(rc), len(expected)) 749 | } 750 | 751 | for n, v := range rc { 752 | ci := expected[n] 753 | if *ci != *v { 754 | t.Fatalf("have %+v, expected %+v", v, ci) 755 | } 756 | } 757 | } 758 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | 5 | In Terminal: 6 | 7 | # build the file 8 | go build -o example 9 | 10 | # set the environment variable 11 | TEST_STRING=Hello 12 | 13 | # run the code 14 | ./example 15 | 16 | # run the code with a different variable for 17 | # this particular execution of the program 18 | TEST_STRING=Goodbye ./example 19 | 20 | */ 21 | 22 | import ( 23 | "log" 24 | 25 | "github.com/joeshaw/envdecode" 26 | ) 27 | 28 | type config struct { 29 | TestString string `env:"TEST_STRING"` 30 | } 31 | 32 | func main() { 33 | 34 | var cfg config 35 | if err := envdecode.Decode(&cfg); err != nil { 36 | log.Fatalf("Failed to decode: %s", err) 37 | } 38 | 39 | log.Println("TEST_STRING:", cfg.TestString) 40 | 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/joeshaw/envdecode 2 | 3 | go 1.12 4 | --------------------------------------------------------------------------------