├── .travis.yml ├── LICENSE ├── MAINTAINERS ├── README.md ├── doc.go ├── env_os.go ├── env_syscall.go ├── envconfig.go ├── envconfig_1.8_test.go ├── envconfig_test.go ├── go.mod ├── testdata ├── custom.txt ├── default_list.txt ├── default_table.txt └── fault.txt ├── usage.go └── usage_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4.x 5 | - 1.5.x 6 | - 1.6.x 7 | - 1.7.x 8 | - 1.8.x 9 | - 1.9.x 10 | - 1.10.x 11 | - 1.11.x 12 | - 1.12.x 13 | - tip 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Kelsey Hightower 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Kelsey Hightower kelsey.hightower@gmail.com github.com/kelseyhightower 2 | Travis Parker travis.parker@gmail.com github.com/teepark 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # envconfig 2 | 3 | [![Build Status](https://travis-ci.org/kelseyhightower/envconfig.svg)](https://travis-ci.org/kelseyhightower/envconfig) 4 | 5 | ```Go 6 | import "github.com/kelseyhightower/envconfig" 7 | ``` 8 | 9 | ## Documentation 10 | 11 | See [godoc](http://godoc.org/github.com/kelseyhightower/envconfig) 12 | 13 | ## Usage 14 | 15 | Set some environment variables: 16 | 17 | ```Bash 18 | export MYAPP_DEBUG=false 19 | export MYAPP_PORT=8080 20 | export MYAPP_USER=Kelsey 21 | export MYAPP_RATE="0.5" 22 | export MYAPP_TIMEOUT="3m" 23 | export MYAPP_USERS="rob,ken,robert" 24 | export MYAPP_COLORCODES="red:1,green:2,blue:3" 25 | ``` 26 | 27 | Write some code: 28 | 29 | ```Go 30 | package main 31 | 32 | import ( 33 | "fmt" 34 | "log" 35 | "time" 36 | 37 | "github.com/kelseyhightower/envconfig" 38 | ) 39 | 40 | type Specification struct { 41 | Debug bool 42 | Port int 43 | User string 44 | Users []string 45 | Rate float32 46 | Timeout time.Duration 47 | ColorCodes map[string]int 48 | } 49 | 50 | func main() { 51 | var s Specification 52 | err := envconfig.Process("myapp", &s) 53 | if err != nil { 54 | log.Fatal(err.Error()) 55 | } 56 | format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nTimeout: %s\n" 57 | _, err = fmt.Printf(format, s.Debug, s.Port, s.User, s.Rate, s.Timeout) 58 | if err != nil { 59 | log.Fatal(err.Error()) 60 | } 61 | 62 | fmt.Println("Users:") 63 | for _, u := range s.Users { 64 | fmt.Printf(" %s\n", u) 65 | } 66 | 67 | fmt.Println("Color codes:") 68 | for k, v := range s.ColorCodes { 69 | fmt.Printf(" %s: %d\n", k, v) 70 | } 71 | } 72 | ``` 73 | 74 | Results: 75 | 76 | ```Bash 77 | Debug: false 78 | Port: 8080 79 | User: Kelsey 80 | Rate: 0.500000 81 | Timeout: 3m0s 82 | Users: 83 | rob 84 | ken 85 | robert 86 | Color codes: 87 | red: 1 88 | green: 2 89 | blue: 3 90 | ``` 91 | 92 | ## Struct Tag Support 93 | 94 | Envconfig supports the use of struct tags to specify alternate, default, and required 95 | environment variables. 96 | 97 | For example, consider the following struct: 98 | 99 | ```Go 100 | type Specification struct { 101 | ManualOverride1 string `envconfig:"manual_override_1"` 102 | DefaultVar string `default:"foobar"` 103 | RequiredVar string `required:"true"` 104 | IgnoredVar string `ignored:"true"` 105 | AutoSplitVar string `split_words:"true"` 106 | RequiredAndAutoSplitVar string `required:"true" split_words:"true"` 107 | } 108 | ``` 109 | 110 | Envconfig has automatic support for CamelCased struct elements when the 111 | `split_words:"true"` tag is supplied. Without this tag, `AutoSplitVar` above 112 | would look for an environment variable called `MYAPP_AUTOSPLITVAR`. With the 113 | setting applied it will look for `MYAPP_AUTO_SPLIT_VAR`. Note that numbers 114 | will get globbed into the previous word. If the setting does not do the 115 | right thing, you may use a manual override. 116 | 117 | Envconfig will process value for `ManualOverride1` by populating it with the 118 | value for `MYAPP_MANUAL_OVERRIDE_1`. Without this struct tag, it would have 119 | instead looked up `MYAPP_MANUALOVERRIDE1`. With the `split_words:"true"` tag 120 | it would have looked up `MYAPP_MANUAL_OVERRIDE1`. 121 | 122 | ```Bash 123 | export MYAPP_MANUAL_OVERRIDE_1="this will be the value" 124 | 125 | # export MYAPP_MANUALOVERRIDE1="and this will not" 126 | ``` 127 | 128 | If envconfig can't find an environment variable value for `MYAPP_DEFAULTVAR`, 129 | it will populate it with "foobar" as a default value. 130 | 131 | If envconfig can't find an environment variable value for `MYAPP_REQUIREDVAR`, 132 | it will return an error when asked to process the struct. If 133 | `MYAPP_REQUIREDVAR` is present but empty, envconfig will not return an error. 134 | 135 | If envconfig can't find an environment variable in the form `PREFIX_MYVAR`, and there 136 | is a struct tag defined, it will try to populate your variable with an environment 137 | variable that directly matches the envconfig tag in your struct definition: 138 | 139 | ```shell 140 | export SERVICE_HOST=127.0.0.1 141 | export MYAPP_DEBUG=true 142 | ``` 143 | ```Go 144 | type Specification struct { 145 | ServiceHost string `envconfig:"SERVICE_HOST"` 146 | Debug bool 147 | } 148 | ``` 149 | 150 | Envconfig won't process a field with the "ignored" tag set to "true", even if a corresponding 151 | environment variable is set. 152 | 153 | ## Supported Struct Field Types 154 | 155 | envconfig supports these struct field types: 156 | 157 | * string 158 | * int8, int16, int32, int64 159 | * bool 160 | * float32, float64 161 | * slices of any supported type 162 | * maps (keys and values of any supported type) 163 | * [encoding.TextUnmarshaler](https://golang.org/pkg/encoding/#TextUnmarshaler) 164 | * [encoding.BinaryUnmarshaler](https://golang.org/pkg/encoding/#BinaryUnmarshaler) 165 | * [time.Duration](https://golang.org/pkg/time/#Duration) 166 | 167 | Embedded structs using these fields are also supported. 168 | 169 | ## Custom Decoders 170 | 171 | Any field whose type (or pointer-to-type) implements `envconfig.Decoder` can 172 | control its own deserialization: 173 | 174 | ```Bash 175 | export DNS_SERVER=8.8.8.8 176 | ``` 177 | 178 | ```Go 179 | type IPDecoder net.IP 180 | 181 | func (ipd *IPDecoder) Decode(value string) error { 182 | *ipd = IPDecoder(net.ParseIP(value)) 183 | return nil 184 | } 185 | 186 | type DNSConfig struct { 187 | Address IPDecoder `envconfig:"DNS_SERVER"` 188 | } 189 | ``` 190 | 191 | Example for decoding the environment variables into map[string][]structName type 192 | 193 | ```Bash 194 | export SMS_PROVIDER_WITH_WEIGHT= `IND=[{"name":"SMSProvider1","weight":70},{"name":"SMSProvider2","weight":30}];US=[{"name":"SMSProvider1","weight":100}]` 195 | ``` 196 | 197 | ```GO 198 | type providerDetails struct { 199 | Name string 200 | Weight int 201 | } 202 | 203 | type SMSProviderDecoder map[string][]providerDetails 204 | 205 | func (sd *SMSProviderDecoder) Decode(value string) error { 206 | smsProvider := map[string][]providerDetails{} 207 | pairs := strings.Split(value, ";") 208 | for _, pair := range pairs { 209 | providerdata := []providerDetails{} 210 | kvpair := strings.Split(pair, "=") 211 | if len(kvpair) != 2 { 212 | return fmt.Errorf("invalid map item: %q", pair) 213 | } 214 | err := json.Unmarshal([]byte(kvpair[1]), &providerdata) 215 | if err != nil { 216 | return fmt.Errorf("invalid map json: %w", err) 217 | } 218 | smsProvider[kvpair[0]] = providerdata 219 | 220 | } 221 | *sd = SMSProviderDecoder(smsProvider) 222 | return nil 223 | } 224 | 225 | type SMSProviderConfig struct { 226 | ProviderWithWeight SMSProviderDecoder `envconfig:"SMS_PROVIDER_WITH_WEIGHT"` 227 | } 228 | ``` 229 | 230 | Also, envconfig will use a `Set(string) error` method like from the 231 | [flag.Value](https://godoc.org/flag#Value) interface if implemented. 232 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Kelsey Hightower. All rights reserved. 2 | // Use of this source code is governed by the MIT License that can be found in 3 | // the LICENSE file. 4 | 5 | // Package envconfig implements decoding of environment variables based on a user 6 | // defined specification. A typical use is using environment variables for 7 | // configuration settings. 8 | package envconfig 9 | -------------------------------------------------------------------------------- /env_os.go: -------------------------------------------------------------------------------- 1 | // +build appengine go1.5 2 | 3 | package envconfig 4 | 5 | import "os" 6 | 7 | var lookupEnv = os.LookupEnv 8 | -------------------------------------------------------------------------------- /env_syscall.go: -------------------------------------------------------------------------------- 1 | // +build !appengine,!go1.5 2 | 3 | package envconfig 4 | 5 | import "syscall" 6 | 7 | var lookupEnv = syscall.Getenv 8 | -------------------------------------------------------------------------------- /envconfig.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Kelsey Hightower. All rights reserved. 2 | // Use of this source code is governed by the MIT License that can be found in 3 | // the LICENSE file. 4 | 5 | package envconfig 6 | 7 | import ( 8 | "encoding" 9 | "errors" 10 | "fmt" 11 | "os" 12 | "reflect" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // ErrInvalidSpecification indicates that a specification is of the wrong type. 20 | var ErrInvalidSpecification = errors.New("specification must be a struct pointer") 21 | 22 | var gatherRegexp = regexp.MustCompile("([^A-Z]+|[A-Z]+[^A-Z]+|[A-Z]+)") 23 | var acronymRegexp = regexp.MustCompile("([A-Z]+)([A-Z][^A-Z]+)") 24 | 25 | // A ParseError occurs when an environment variable cannot be converted to 26 | // the type required by a struct field during assignment. 27 | type ParseError struct { 28 | KeyName string 29 | FieldName string 30 | TypeName string 31 | Value string 32 | Err error 33 | } 34 | 35 | // Decoder has the same semantics as Setter, but takes higher precedence. 36 | // It is provided for historical compatibility. 37 | type Decoder interface { 38 | Decode(value string) error 39 | } 40 | 41 | // Setter is implemented by types can self-deserialize values. 42 | // Any type that implements flag.Value also implements Setter. 43 | type Setter interface { 44 | Set(value string) error 45 | } 46 | 47 | func (e *ParseError) Error() string { 48 | return fmt.Sprintf("envconfig.Process: assigning %[1]s to %[2]s: converting '%[3]s' to type %[4]s. details: %[5]s", e.KeyName, e.FieldName, e.Value, e.TypeName, e.Err) 49 | } 50 | 51 | // varInfo maintains information about the configuration variable 52 | type varInfo struct { 53 | Name string 54 | Alt string 55 | Key string 56 | Field reflect.Value 57 | Tags reflect.StructTag 58 | } 59 | 60 | // GatherInfo gathers information about the specified struct 61 | func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) { 62 | s := reflect.ValueOf(spec) 63 | 64 | if s.Kind() != reflect.Ptr { 65 | return nil, ErrInvalidSpecification 66 | } 67 | s = s.Elem() 68 | if s.Kind() != reflect.Struct { 69 | return nil, ErrInvalidSpecification 70 | } 71 | typeOfSpec := s.Type() 72 | 73 | // over allocate an info array, we will extend if needed later 74 | infos := make([]varInfo, 0, s.NumField()) 75 | for i := 0; i < s.NumField(); i++ { 76 | f := s.Field(i) 77 | ftype := typeOfSpec.Field(i) 78 | if !f.CanSet() || isTrue(ftype.Tag.Get("ignored")) { 79 | continue 80 | } 81 | 82 | for f.Kind() == reflect.Ptr { 83 | if f.IsNil() { 84 | if f.Type().Elem().Kind() != reflect.Struct { 85 | // nil pointer to a non-struct: leave it alone 86 | break 87 | } 88 | // nil pointer to struct: create a zero instance 89 | f.Set(reflect.New(f.Type().Elem())) 90 | } 91 | f = f.Elem() 92 | } 93 | 94 | // Capture information about the config variable 95 | info := varInfo{ 96 | Name: ftype.Name, 97 | Field: f, 98 | Tags: ftype.Tag, 99 | Alt: strings.ToUpper(ftype.Tag.Get("envconfig")), 100 | } 101 | 102 | // Default to the field name as the env var name (will be upcased) 103 | info.Key = info.Name 104 | 105 | // Best effort to un-pick camel casing as separate words 106 | if isTrue(ftype.Tag.Get("split_words")) { 107 | words := gatherRegexp.FindAllStringSubmatch(ftype.Name, -1) 108 | if len(words) > 0 { 109 | var name []string 110 | for _, words := range words { 111 | if m := acronymRegexp.FindStringSubmatch(words[0]); len(m) == 3 { 112 | name = append(name, m[1], m[2]) 113 | } else { 114 | name = append(name, words[0]) 115 | } 116 | } 117 | 118 | info.Key = strings.Join(name, "_") 119 | } 120 | } 121 | if info.Alt != "" { 122 | info.Key = info.Alt 123 | } 124 | if prefix != "" { 125 | info.Key = fmt.Sprintf("%s_%s", prefix, info.Key) 126 | } 127 | info.Key = strings.ToUpper(info.Key) 128 | infos = append(infos, info) 129 | 130 | if f.Kind() == reflect.Struct { 131 | // honor Decode if present 132 | if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil && binaryUnmarshaler(f) == nil { 133 | innerPrefix := prefix 134 | if !ftype.Anonymous { 135 | innerPrefix = info.Key 136 | } 137 | 138 | embeddedPtr := f.Addr().Interface() 139 | embeddedInfos, err := gatherInfo(innerPrefix, embeddedPtr) 140 | if err != nil { 141 | return nil, err 142 | } 143 | infos = append(infos[:len(infos)-1], embeddedInfos...) 144 | 145 | continue 146 | } 147 | } 148 | } 149 | return infos, nil 150 | } 151 | 152 | // CheckDisallowed checks that no environment variables with the prefix are set 153 | // that we don't know how or want to parse. This is likely only meaningful with 154 | // a non-empty prefix. 155 | func CheckDisallowed(prefix string, spec interface{}) error { 156 | infos, err := gatherInfo(prefix, spec) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | vars := make(map[string]struct{}) 162 | for _, info := range infos { 163 | vars[info.Key] = struct{}{} 164 | } 165 | 166 | if prefix != "" { 167 | prefix = strings.ToUpper(prefix) + "_" 168 | } 169 | 170 | for _, env := range os.Environ() { 171 | if !strings.HasPrefix(env, prefix) { 172 | continue 173 | } 174 | v := strings.SplitN(env, "=", 2)[0] 175 | if _, found := vars[v]; !found { 176 | return fmt.Errorf("unknown environment variable %s", v) 177 | } 178 | } 179 | 180 | return nil 181 | } 182 | 183 | // Process populates the specified struct based on environment variables 184 | func Process(prefix string, spec interface{}) error { 185 | infos, err := gatherInfo(prefix, spec) 186 | 187 | for _, info := range infos { 188 | 189 | // `os.Getenv` cannot differentiate between an explicitly set empty value 190 | // and an unset value. `os.LookupEnv` is preferred to `syscall.Getenv`, 191 | // but it is only available in go1.5 or newer. We're using Go build tags 192 | // here to use os.LookupEnv for >=go1.5 193 | value, ok := lookupEnv(info.Key) 194 | if !ok && info.Alt != "" { 195 | value, ok = lookupEnv(info.Alt) 196 | } 197 | 198 | def := info.Tags.Get("default") 199 | if def != "" && !ok { 200 | value = def 201 | } 202 | 203 | req := info.Tags.Get("required") 204 | if !ok && def == "" { 205 | if isTrue(req) { 206 | key := info.Key 207 | if info.Alt != "" { 208 | key = info.Alt 209 | } 210 | return fmt.Errorf("required key %s missing value", key) 211 | } 212 | continue 213 | } 214 | 215 | err = processField(value, info.Field) 216 | if err != nil { 217 | return &ParseError{ 218 | KeyName: info.Key, 219 | FieldName: info.Name, 220 | TypeName: info.Field.Type().String(), 221 | Value: value, 222 | Err: err, 223 | } 224 | } 225 | } 226 | 227 | return err 228 | } 229 | 230 | // MustProcess is the same as Process but panics if an error occurs 231 | func MustProcess(prefix string, spec interface{}) { 232 | if err := Process(prefix, spec); err != nil { 233 | panic(err) 234 | } 235 | } 236 | 237 | func processField(value string, field reflect.Value) error { 238 | typ := field.Type() 239 | 240 | decoder := decoderFrom(field) 241 | if decoder != nil { 242 | return decoder.Decode(value) 243 | } 244 | // look for Set method if Decode not defined 245 | setter := setterFrom(field) 246 | if setter != nil { 247 | return setter.Set(value) 248 | } 249 | 250 | if t := textUnmarshaler(field); t != nil { 251 | return t.UnmarshalText([]byte(value)) 252 | } 253 | 254 | if b := binaryUnmarshaler(field); b != nil { 255 | return b.UnmarshalBinary([]byte(value)) 256 | } 257 | 258 | if typ.Kind() == reflect.Ptr { 259 | typ = typ.Elem() 260 | if field.IsNil() { 261 | field.Set(reflect.New(typ)) 262 | } 263 | field = field.Elem() 264 | } 265 | 266 | switch typ.Kind() { 267 | case reflect.String: 268 | field.SetString(value) 269 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 270 | var ( 271 | val int64 272 | err error 273 | ) 274 | if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" { 275 | var d time.Duration 276 | d, err = time.ParseDuration(value) 277 | val = int64(d) 278 | } else { 279 | val, err = strconv.ParseInt(value, 0, typ.Bits()) 280 | } 281 | if err != nil { 282 | return err 283 | } 284 | 285 | field.SetInt(val) 286 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 287 | val, err := strconv.ParseUint(value, 0, typ.Bits()) 288 | if err != nil { 289 | return err 290 | } 291 | field.SetUint(val) 292 | case reflect.Bool: 293 | val, err := strconv.ParseBool(value) 294 | if err != nil { 295 | return err 296 | } 297 | field.SetBool(val) 298 | case reflect.Float32, reflect.Float64: 299 | val, err := strconv.ParseFloat(value, typ.Bits()) 300 | if err != nil { 301 | return err 302 | } 303 | field.SetFloat(val) 304 | case reflect.Slice: 305 | sl := reflect.MakeSlice(typ, 0, 0) 306 | if typ.Elem().Kind() == reflect.Uint8 { 307 | sl = reflect.ValueOf([]byte(value)) 308 | } else if strings.TrimSpace(value) != "" { 309 | vals := strings.Split(value, ",") 310 | sl = reflect.MakeSlice(typ, len(vals), len(vals)) 311 | for i, val := range vals { 312 | err := processField(val, sl.Index(i)) 313 | if err != nil { 314 | return err 315 | } 316 | } 317 | } 318 | field.Set(sl) 319 | case reflect.Map: 320 | mp := reflect.MakeMap(typ) 321 | if strings.TrimSpace(value) != "" { 322 | pairs := strings.Split(value, ",") 323 | for _, pair := range pairs { 324 | kvpair := strings.Split(pair, ":") 325 | if len(kvpair) != 2 { 326 | return fmt.Errorf("invalid map item: %q", pair) 327 | } 328 | k := reflect.New(typ.Key()).Elem() 329 | err := processField(kvpair[0], k) 330 | if err != nil { 331 | return err 332 | } 333 | v := reflect.New(typ.Elem()).Elem() 334 | err = processField(kvpair[1], v) 335 | if err != nil { 336 | return err 337 | } 338 | mp.SetMapIndex(k, v) 339 | } 340 | } 341 | field.Set(mp) 342 | } 343 | 344 | return nil 345 | } 346 | 347 | func interfaceFrom(field reflect.Value, fn func(interface{}, *bool)) { 348 | // it may be impossible for a struct field to fail this check 349 | if !field.CanInterface() { 350 | return 351 | } 352 | var ok bool 353 | fn(field.Interface(), &ok) 354 | if !ok && field.CanAddr() { 355 | fn(field.Addr().Interface(), &ok) 356 | } 357 | } 358 | 359 | func decoderFrom(field reflect.Value) (d Decoder) { 360 | interfaceFrom(field, func(v interface{}, ok *bool) { d, *ok = v.(Decoder) }) 361 | return d 362 | } 363 | 364 | func setterFrom(field reflect.Value) (s Setter) { 365 | interfaceFrom(field, func(v interface{}, ok *bool) { s, *ok = v.(Setter) }) 366 | return s 367 | } 368 | 369 | func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) { 370 | interfaceFrom(field, func(v interface{}, ok *bool) { t, *ok = v.(encoding.TextUnmarshaler) }) 371 | return t 372 | } 373 | 374 | func binaryUnmarshaler(field reflect.Value) (b encoding.BinaryUnmarshaler) { 375 | interfaceFrom(field, func(v interface{}, ok *bool) { b, *ok = v.(encoding.BinaryUnmarshaler) }) 376 | return b 377 | } 378 | 379 | func isTrue(s string) bool { 380 | b, _ := strconv.ParseBool(s) 381 | return b 382 | } 383 | -------------------------------------------------------------------------------- /envconfig_1.8_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.8 2 | 3 | package envconfig 4 | 5 | import ( 6 | "errors" 7 | "net/url" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | type SpecWithURL struct { 13 | UrlValue url.URL 14 | UrlPointer *url.URL 15 | } 16 | 17 | func TestParseURL(t *testing.T) { 18 | var s SpecWithURL 19 | 20 | os.Clearenv() 21 | os.Setenv("ENV_CONFIG_URLVALUE", "https://github.com/kelseyhightower/envconfig") 22 | os.Setenv("ENV_CONFIG_URLPOINTER", "https://github.com/kelseyhightower/envconfig") 23 | 24 | err := Process("env_config", &s) 25 | if err != nil { 26 | t.Fatal("unexpected error:", err) 27 | } 28 | 29 | u, err := url.Parse("https://github.com/kelseyhightower/envconfig") 30 | if err != nil { 31 | t.Fatalf("unexpected error: %v", err) 32 | } 33 | 34 | if s.UrlValue != *u { 35 | t.Errorf("expected %q, got %q", u, s.UrlValue.String()) 36 | } 37 | 38 | if *s.UrlPointer != *u { 39 | t.Errorf("expected %q, got %q", u, s.UrlPointer) 40 | } 41 | } 42 | 43 | func TestParseURLError(t *testing.T) { 44 | var s SpecWithURL 45 | 46 | os.Clearenv() 47 | os.Setenv("ENV_CONFIG_URLPOINTER", "http_://foo") 48 | 49 | err := Process("env_config", &s) 50 | 51 | v, ok := err.(*ParseError) 52 | if !ok { 53 | t.Fatalf("expected ParseError, got %T %v", err, err) 54 | } 55 | if v.FieldName != "UrlPointer" { 56 | t.Errorf("expected %s, got %v", "UrlPointer", v.FieldName) 57 | } 58 | 59 | expectedUnerlyingError := url.Error{ 60 | Op: "parse", 61 | URL: "http_://foo", 62 | Err: errors.New("first path segment in URL cannot contain colon"), 63 | } 64 | 65 | if v.Err.Error() != expectedUnerlyingError.Error() { 66 | t.Errorf("expected %q, got %q", expectedUnerlyingError, v.Err) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /envconfig_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Kelsey Hightower. All rights reserved. 2 | // Use of this source code is governed by the MIT License that can be found in 3 | // the LICENSE file. 4 | 5 | package envconfig 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "net/url" 11 | "os" 12 | "strings" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | type HonorDecodeInStruct struct { 18 | Value string 19 | } 20 | 21 | func (h *HonorDecodeInStruct) Decode(env string) error { 22 | h.Value = "decoded" 23 | return nil 24 | } 25 | 26 | type CustomURL struct { 27 | Value *url.URL 28 | } 29 | 30 | func (cu *CustomURL) UnmarshalBinary(data []byte) error { 31 | u, err := url.Parse(string(data)) 32 | cu.Value = u 33 | return err 34 | } 35 | 36 | type Specification struct { 37 | Embedded `desc:"can we document a struct"` 38 | EmbeddedButIgnored `ignored:"true"` 39 | Debug bool 40 | Port int 41 | Rate float32 42 | User string 43 | TTL uint32 44 | Timeout time.Duration 45 | AdminUsers []string 46 | MagicNumbers []int 47 | EmptyNumbers []int 48 | ByteSlice []byte 49 | ColorCodes map[string]int 50 | MultiWordVar string 51 | MultiWordVarWithAutoSplit uint32 `split_words:"true"` 52 | MultiWordACRWithAutoSplit uint32 `split_words:"true"` 53 | SomePointer *string 54 | SomePointerWithDefault *string `default:"foo2baz" desc:"foorbar is the word"` 55 | MultiWordVarWithAlt string `envconfig:"MULTI_WORD_VAR_WITH_ALT" desc:"what alt"` 56 | MultiWordVarWithLowerCaseAlt string `envconfig:"multi_word_var_with_lower_case_alt"` 57 | NoPrefixWithAlt string `envconfig:"SERVICE_HOST"` 58 | DefaultVar string `default:"foobar"` 59 | RequiredVar string `required:"True"` 60 | NoPrefixDefault string `envconfig:"BROKER" default:"127.0.0.1"` 61 | RequiredDefault string `required:"true" default:"foo2bar"` 62 | Ignored string `ignored:"true"` 63 | NestedSpecification struct { 64 | Property string `envconfig:"inner"` 65 | PropertyWithDefault string `default:"fuzzybydefault"` 66 | } `envconfig:"outer"` 67 | AfterNested string 68 | DecodeStruct HonorDecodeInStruct `envconfig:"honor"` 69 | Datetime time.Time 70 | MapField map[string]string `default:"one:two,three:four"` 71 | UrlValue CustomURL 72 | UrlPointer *CustomURL 73 | } 74 | 75 | type Embedded struct { 76 | Enabled bool `desc:"some embedded value"` 77 | EmbeddedPort int 78 | MultiWordVar string 79 | MultiWordVarWithAlt string `envconfig:"MULTI_WITH_DIFFERENT_ALT"` 80 | EmbeddedAlt string `envconfig:"EMBEDDED_WITH_ALT"` 81 | EmbeddedIgnored string `ignored:"true"` 82 | } 83 | 84 | type EmbeddedButIgnored struct { 85 | FirstEmbeddedButIgnored string 86 | SecondEmbeddedButIgnored string 87 | } 88 | 89 | func TestProcess(t *testing.T) { 90 | var s Specification 91 | os.Clearenv() 92 | os.Setenv("ENV_CONFIG_DEBUG", "true") 93 | os.Setenv("ENV_CONFIG_PORT", "8080") 94 | os.Setenv("ENV_CONFIG_RATE", "0.5") 95 | os.Setenv("ENV_CONFIG_USER", "Kelsey") 96 | os.Setenv("ENV_CONFIG_TIMEOUT", "2m") 97 | os.Setenv("ENV_CONFIG_ADMINUSERS", "John,Adam,Will") 98 | os.Setenv("ENV_CONFIG_MAGICNUMBERS", "5,10,20") 99 | os.Setenv("ENV_CONFIG_EMPTYNUMBERS", "") 100 | os.Setenv("ENV_CONFIG_BYTESLICE", "this is a test value") 101 | os.Setenv("ENV_CONFIG_COLORCODES", "red:1,green:2,blue:3") 102 | os.Setenv("SERVICE_HOST", "127.0.0.1") 103 | os.Setenv("ENV_CONFIG_TTL", "30") 104 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 105 | os.Setenv("ENV_CONFIG_IGNORED", "was-not-ignored") 106 | os.Setenv("ENV_CONFIG_OUTER_INNER", "iamnested") 107 | os.Setenv("ENV_CONFIG_AFTERNESTED", "after") 108 | os.Setenv("ENV_CONFIG_HONOR", "honor") 109 | os.Setenv("ENV_CONFIG_DATETIME", "2016-08-16T18:57:05Z") 110 | os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT", "24") 111 | os.Setenv("ENV_CONFIG_MULTI_WORD_ACR_WITH_AUTO_SPLIT", "25") 112 | os.Setenv("ENV_CONFIG_URLVALUE", "https://github.com/kelseyhightower/envconfig") 113 | os.Setenv("ENV_CONFIG_URLPOINTER", "https://github.com/kelseyhightower/envconfig") 114 | err := Process("env_config", &s) 115 | if err != nil { 116 | t.Error(err.Error()) 117 | } 118 | if s.NoPrefixWithAlt != "127.0.0.1" { 119 | t.Errorf("expected %v, got %v", "127.0.0.1", s.NoPrefixWithAlt) 120 | } 121 | if !s.Debug { 122 | t.Errorf("expected %v, got %v", true, s.Debug) 123 | } 124 | if s.Port != 8080 { 125 | t.Errorf("expected %d, got %v", 8080, s.Port) 126 | } 127 | if s.Rate != 0.5 { 128 | t.Errorf("expected %f, got %v", 0.5, s.Rate) 129 | } 130 | if s.TTL != 30 { 131 | t.Errorf("expected %d, got %v", 30, s.TTL) 132 | } 133 | if s.User != "Kelsey" { 134 | t.Errorf("expected %s, got %s", "Kelsey", s.User) 135 | } 136 | if s.Timeout != 2*time.Minute { 137 | t.Errorf("expected %s, got %s", 2*time.Minute, s.Timeout) 138 | } 139 | if s.RequiredVar != "foo" { 140 | t.Errorf("expected %s, got %s", "foo", s.RequiredVar) 141 | } 142 | if len(s.AdminUsers) != 3 || 143 | s.AdminUsers[0] != "John" || 144 | s.AdminUsers[1] != "Adam" || 145 | s.AdminUsers[2] != "Will" { 146 | t.Errorf("expected %#v, got %#v", []string{"John", "Adam", "Will"}, s.AdminUsers) 147 | } 148 | if len(s.MagicNumbers) != 3 || 149 | s.MagicNumbers[0] != 5 || 150 | s.MagicNumbers[1] != 10 || 151 | s.MagicNumbers[2] != 20 { 152 | t.Errorf("expected %#v, got %#v", []int{5, 10, 20}, s.MagicNumbers) 153 | } 154 | if len(s.EmptyNumbers) != 0 { 155 | t.Errorf("expected %#v, got %#v", []int{}, s.EmptyNumbers) 156 | } 157 | expected := "this is a test value" 158 | if string(s.ByteSlice) != expected { 159 | t.Errorf("expected %v, got %v", expected, string(s.ByteSlice)) 160 | } 161 | if s.Ignored != "" { 162 | t.Errorf("expected empty string, got %#v", s.Ignored) 163 | } 164 | 165 | if len(s.ColorCodes) != 3 || 166 | s.ColorCodes["red"] != 1 || 167 | s.ColorCodes["green"] != 2 || 168 | s.ColorCodes["blue"] != 3 { 169 | t.Errorf( 170 | "expected %#v, got %#v", 171 | map[string]int{ 172 | "red": 1, 173 | "green": 2, 174 | "blue": 3, 175 | }, 176 | s.ColorCodes, 177 | ) 178 | } 179 | 180 | if s.NestedSpecification.Property != "iamnested" { 181 | t.Errorf("expected '%s' string, got %#v", "iamnested", s.NestedSpecification.Property) 182 | } 183 | 184 | if s.NestedSpecification.PropertyWithDefault != "fuzzybydefault" { 185 | t.Errorf("expected default '%s' string, got %#v", "fuzzybydefault", s.NestedSpecification.PropertyWithDefault) 186 | } 187 | 188 | if s.AfterNested != "after" { 189 | t.Errorf("expected default '%s' string, got %#v", "after", s.AfterNested) 190 | } 191 | 192 | if s.DecodeStruct.Value != "decoded" { 193 | t.Errorf("expected default '%s' string, got %#v", "decoded", s.DecodeStruct.Value) 194 | } 195 | 196 | if expected := time.Date(2016, 8, 16, 18, 57, 05, 0, time.UTC); !s.Datetime.Equal(expected) { 197 | t.Errorf("expected %s, got %s", expected.Format(time.RFC3339), s.Datetime.Format(time.RFC3339)) 198 | } 199 | 200 | if s.MultiWordVarWithAutoSplit != 24 { 201 | t.Errorf("expected %q, got %q", 24, s.MultiWordVarWithAutoSplit) 202 | } 203 | 204 | if s.MultiWordACRWithAutoSplit != 25 { 205 | t.Errorf("expected %d, got %d", 25, s.MultiWordACRWithAutoSplit) 206 | } 207 | 208 | u, err := url.Parse("https://github.com/kelseyhightower/envconfig") 209 | if err != nil { 210 | t.Fatalf("unexpected error: %v", err) 211 | } 212 | 213 | if *s.UrlValue.Value != *u { 214 | t.Errorf("expected %q, got %q", u, s.UrlValue.Value.String()) 215 | } 216 | 217 | if *s.UrlPointer.Value != *u { 218 | t.Errorf("expected %q, got %q", u, s.UrlPointer.Value.String()) 219 | } 220 | } 221 | 222 | func TestParseErrorBool(t *testing.T) { 223 | var s Specification 224 | os.Clearenv() 225 | os.Setenv("ENV_CONFIG_DEBUG", "string") 226 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 227 | err := Process("env_config", &s) 228 | v, ok := err.(*ParseError) 229 | if !ok { 230 | t.Errorf("expected ParseError, got %v", v) 231 | } 232 | if v.FieldName != "Debug" { 233 | t.Errorf("expected %s, got %v", "Debug", v.FieldName) 234 | } 235 | if s.Debug != false { 236 | t.Errorf("expected %v, got %v", false, s.Debug) 237 | } 238 | } 239 | 240 | func TestParseErrorFloat32(t *testing.T) { 241 | var s Specification 242 | os.Clearenv() 243 | os.Setenv("ENV_CONFIG_RATE", "string") 244 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 245 | err := Process("env_config", &s) 246 | v, ok := err.(*ParseError) 247 | if !ok { 248 | t.Errorf("expected ParseError, got %v", v) 249 | } 250 | if v.FieldName != "Rate" { 251 | t.Errorf("expected %s, got %v", "Rate", v.FieldName) 252 | } 253 | if s.Rate != 0 { 254 | t.Errorf("expected %v, got %v", 0, s.Rate) 255 | } 256 | } 257 | 258 | func TestParseErrorInt(t *testing.T) { 259 | var s Specification 260 | os.Clearenv() 261 | os.Setenv("ENV_CONFIG_PORT", "string") 262 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 263 | err := Process("env_config", &s) 264 | v, ok := err.(*ParseError) 265 | if !ok { 266 | t.Errorf("expected ParseError, got %v", v) 267 | } 268 | if v.FieldName != "Port" { 269 | t.Errorf("expected %s, got %v", "Port", v.FieldName) 270 | } 271 | if s.Port != 0 { 272 | t.Errorf("expected %v, got %v", 0, s.Port) 273 | } 274 | } 275 | 276 | func TestParseErrorUint(t *testing.T) { 277 | var s Specification 278 | os.Clearenv() 279 | os.Setenv("ENV_CONFIG_TTL", "-30") 280 | err := Process("env_config", &s) 281 | v, ok := err.(*ParseError) 282 | if !ok { 283 | t.Errorf("expected ParseError, got %v", v) 284 | } 285 | if v.FieldName != "TTL" { 286 | t.Errorf("expected %s, got %v", "TTL", v.FieldName) 287 | } 288 | if s.TTL != 0 { 289 | t.Errorf("expected %v, got %v", 0, s.TTL) 290 | } 291 | } 292 | 293 | func TestParseErrorSplitWords(t *testing.T) { 294 | var s Specification 295 | os.Clearenv() 296 | os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT", "shakespeare") 297 | err := Process("env_config", &s) 298 | v, ok := err.(*ParseError) 299 | if !ok { 300 | t.Errorf("expected ParseError, got %v", v) 301 | } 302 | if v.FieldName != "MultiWordVarWithAutoSplit" { 303 | t.Errorf("expected %s, got %v", "", v.FieldName) 304 | } 305 | if s.MultiWordVarWithAutoSplit != 0 { 306 | t.Errorf("expected %v, got %v", 0, s.MultiWordVarWithAutoSplit) 307 | } 308 | } 309 | 310 | func TestErrInvalidSpecification(t *testing.T) { 311 | m := make(map[string]string) 312 | err := Process("env_config", &m) 313 | if err != ErrInvalidSpecification { 314 | t.Errorf("expected %v, got %v", ErrInvalidSpecification, err) 315 | } 316 | } 317 | 318 | func TestUnsetVars(t *testing.T) { 319 | var s Specification 320 | os.Clearenv() 321 | os.Setenv("USER", "foo") 322 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 323 | if err := Process("env_config", &s); err != nil { 324 | t.Error(err.Error()) 325 | } 326 | 327 | // If the var is not defined the non-prefixed version should not be used 328 | // unless the struct tag says so 329 | if s.User != "" { 330 | t.Errorf("expected %q, got %q", "", s.User) 331 | } 332 | } 333 | 334 | func TestAlternateVarNames(t *testing.T) { 335 | var s Specification 336 | os.Clearenv() 337 | os.Setenv("ENV_CONFIG_MULTI_WORD_VAR", "foo") 338 | os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT", "bar") 339 | os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT", "baz") 340 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 341 | if err := Process("env_config", &s); err != nil { 342 | t.Error(err.Error()) 343 | } 344 | 345 | // Setting the alt version of the var in the environment has no effect if 346 | // the struct tag is not supplied 347 | if s.MultiWordVar != "" { 348 | t.Errorf("expected %q, got %q", "", s.MultiWordVar) 349 | } 350 | 351 | // Setting the alt version of the var in the environment correctly sets 352 | // the value if the struct tag IS supplied 353 | if s.MultiWordVarWithAlt != "bar" { 354 | t.Errorf("expected %q, got %q", "bar", s.MultiWordVarWithAlt) 355 | } 356 | 357 | // Alt value is not case sensitive and is treated as all uppercase 358 | if s.MultiWordVarWithLowerCaseAlt != "baz" { 359 | t.Errorf("expected %q, got %q", "baz", s.MultiWordVarWithLowerCaseAlt) 360 | } 361 | } 362 | 363 | func TestRequiredVar(t *testing.T) { 364 | var s Specification 365 | os.Clearenv() 366 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foobar") 367 | if err := Process("env_config", &s); err != nil { 368 | t.Error(err.Error()) 369 | } 370 | 371 | if s.RequiredVar != "foobar" { 372 | t.Errorf("expected %s, got %s", "foobar", s.RequiredVar) 373 | } 374 | } 375 | 376 | func TestRequiredMissing(t *testing.T) { 377 | var s Specification 378 | os.Clearenv() 379 | 380 | err := Process("env_config", &s) 381 | if err == nil { 382 | t.Error("no failure when missing required variable") 383 | } 384 | } 385 | 386 | func TestBlankDefaultVar(t *testing.T) { 387 | var s Specification 388 | os.Clearenv() 389 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "requiredvalue") 390 | if err := Process("env_config", &s); err != nil { 391 | t.Error(err.Error()) 392 | } 393 | 394 | if s.DefaultVar != "foobar" { 395 | t.Errorf("expected %s, got %s", "foobar", s.DefaultVar) 396 | } 397 | 398 | if *s.SomePointerWithDefault != "foo2baz" { 399 | t.Errorf("expected %s, got %s", "foo2baz", *s.SomePointerWithDefault) 400 | } 401 | } 402 | 403 | func TestNonBlankDefaultVar(t *testing.T) { 404 | var s Specification 405 | os.Clearenv() 406 | os.Setenv("ENV_CONFIG_DEFAULTVAR", "nondefaultval") 407 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "requiredvalue") 408 | if err := Process("env_config", &s); err != nil { 409 | t.Error(err.Error()) 410 | } 411 | 412 | if s.DefaultVar != "nondefaultval" { 413 | t.Errorf("expected %s, got %s", "nondefaultval", s.DefaultVar) 414 | } 415 | } 416 | 417 | func TestExplicitBlankDefaultVar(t *testing.T) { 418 | var s Specification 419 | os.Clearenv() 420 | os.Setenv("ENV_CONFIG_DEFAULTVAR", "") 421 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "") 422 | 423 | if err := Process("env_config", &s); err != nil { 424 | t.Error(err.Error()) 425 | } 426 | 427 | if s.DefaultVar != "" { 428 | t.Errorf("expected %s, got %s", "\"\"", s.DefaultVar) 429 | } 430 | } 431 | 432 | func TestAlternateNameDefaultVar(t *testing.T) { 433 | var s Specification 434 | os.Clearenv() 435 | os.Setenv("BROKER", "betterbroker") 436 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 437 | if err := Process("env_config", &s); err != nil { 438 | t.Error(err.Error()) 439 | } 440 | 441 | if s.NoPrefixDefault != "betterbroker" { 442 | t.Errorf("expected %q, got %q", "betterbroker", s.NoPrefixDefault) 443 | } 444 | 445 | os.Clearenv() 446 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 447 | if err := Process("env_config", &s); err != nil { 448 | t.Error(err.Error()) 449 | } 450 | 451 | if s.NoPrefixDefault != "127.0.0.1" { 452 | t.Errorf("expected %q, got %q", "127.0.0.1", s.NoPrefixDefault) 453 | } 454 | } 455 | 456 | func TestRequiredDefault(t *testing.T) { 457 | var s Specification 458 | os.Clearenv() 459 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 460 | if err := Process("env_config", &s); err != nil { 461 | t.Error(err.Error()) 462 | } 463 | 464 | if s.RequiredDefault != "foo2bar" { 465 | t.Errorf("expected %q, got %q", "foo2bar", s.RequiredDefault) 466 | } 467 | } 468 | 469 | func TestPointerFieldBlank(t *testing.T) { 470 | var s Specification 471 | os.Clearenv() 472 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 473 | if err := Process("env_config", &s); err != nil { 474 | t.Error(err.Error()) 475 | } 476 | 477 | if s.SomePointer != nil { 478 | t.Errorf("expected , got %q", *s.SomePointer) 479 | } 480 | } 481 | 482 | func TestEmptyMapFieldOverride(t *testing.T) { 483 | var s Specification 484 | os.Clearenv() 485 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 486 | os.Setenv("ENV_CONFIG_MAPFIELD", "") 487 | if err := Process("env_config", &s); err != nil { 488 | t.Error(err.Error()) 489 | } 490 | 491 | if s.MapField == nil { 492 | t.Error("expected empty map, got ") 493 | } 494 | 495 | if len(s.MapField) != 0 { 496 | t.Errorf("expected empty map, got map of size %d", len(s.MapField)) 497 | } 498 | } 499 | 500 | func TestMustProcess(t *testing.T) { 501 | var s Specification 502 | os.Clearenv() 503 | os.Setenv("ENV_CONFIG_DEBUG", "true") 504 | os.Setenv("ENV_CONFIG_PORT", "8080") 505 | os.Setenv("ENV_CONFIG_RATE", "0.5") 506 | os.Setenv("ENV_CONFIG_USER", "Kelsey") 507 | os.Setenv("SERVICE_HOST", "127.0.0.1") 508 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 509 | MustProcess("env_config", &s) 510 | 511 | defer func() { 512 | if err := recover(); err != nil { 513 | return 514 | } 515 | 516 | t.Error("expected panic") 517 | }() 518 | m := make(map[string]string) 519 | MustProcess("env_config", &m) 520 | } 521 | 522 | func TestEmbeddedStruct(t *testing.T) { 523 | var s Specification 524 | os.Clearenv() 525 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "required") 526 | os.Setenv("ENV_CONFIG_ENABLED", "true") 527 | os.Setenv("ENV_CONFIG_EMBEDDEDPORT", "1234") 528 | os.Setenv("ENV_CONFIG_MULTIWORDVAR", "foo") 529 | os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT", "bar") 530 | os.Setenv("ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT", "baz") 531 | os.Setenv("ENV_CONFIG_EMBEDDED_WITH_ALT", "foobar") 532 | os.Setenv("ENV_CONFIG_SOMEPOINTER", "foobaz") 533 | os.Setenv("ENV_CONFIG_EMBEDDED_IGNORED", "was-not-ignored") 534 | if err := Process("env_config", &s); err != nil { 535 | t.Error(err.Error()) 536 | } 537 | if !s.Enabled { 538 | t.Errorf("expected %v, got %v", true, s.Enabled) 539 | } 540 | if s.EmbeddedPort != 1234 { 541 | t.Errorf("expected %d, got %v", 1234, s.EmbeddedPort) 542 | } 543 | if s.MultiWordVar != "foo" { 544 | t.Errorf("expected %s, got %s", "foo", s.MultiWordVar) 545 | } 546 | if s.Embedded.MultiWordVar != "foo" { 547 | t.Errorf("expected %s, got %s", "foo", s.Embedded.MultiWordVar) 548 | } 549 | if s.MultiWordVarWithAlt != "bar" { 550 | t.Errorf("expected %s, got %s", "bar", s.MultiWordVarWithAlt) 551 | } 552 | if s.Embedded.MultiWordVarWithAlt != "baz" { 553 | t.Errorf("expected %s, got %s", "baz", s.Embedded.MultiWordVarWithAlt) 554 | } 555 | if s.EmbeddedAlt != "foobar" { 556 | t.Errorf("expected %s, got %s", "foobar", s.EmbeddedAlt) 557 | } 558 | if *s.SomePointer != "foobaz" { 559 | t.Errorf("expected %s, got %s", "foobaz", *s.SomePointer) 560 | } 561 | if s.EmbeddedIgnored != "" { 562 | t.Errorf("expected empty string, got %#v", s.Ignored) 563 | } 564 | } 565 | 566 | func TestEmbeddedButIgnoredStruct(t *testing.T) { 567 | var s Specification 568 | os.Clearenv() 569 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "required") 570 | os.Setenv("ENV_CONFIG_FIRSTEMBEDDEDBUTIGNORED", "was-not-ignored") 571 | os.Setenv("ENV_CONFIG_SECONDEMBEDDEDBUTIGNORED", "was-not-ignored") 572 | if err := Process("env_config", &s); err != nil { 573 | t.Error(err.Error()) 574 | } 575 | if s.FirstEmbeddedButIgnored != "" { 576 | t.Errorf("expected empty string, got %#v", s.Ignored) 577 | } 578 | if s.SecondEmbeddedButIgnored != "" { 579 | t.Errorf("expected empty string, got %#v", s.Ignored) 580 | } 581 | } 582 | 583 | func TestNonPointerFailsProperly(t *testing.T) { 584 | var s Specification 585 | os.Clearenv() 586 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "snap") 587 | 588 | err := Process("env_config", s) 589 | if err != ErrInvalidSpecification { 590 | t.Errorf("non-pointer should fail with ErrInvalidSpecification, was instead %s", err) 591 | } 592 | } 593 | 594 | func TestCustomValueFields(t *testing.T) { 595 | var s struct { 596 | Foo string 597 | Bar bracketed 598 | Baz quoted 599 | Struct setterStruct 600 | } 601 | 602 | // Set would panic when the receiver is nil, 603 | // so make sure it has an initial value to replace. 604 | s.Baz = quoted{new(bracketed)} 605 | 606 | os.Clearenv() 607 | os.Setenv("ENV_CONFIG_FOO", "foo") 608 | os.Setenv("ENV_CONFIG_BAR", "bar") 609 | os.Setenv("ENV_CONFIG_BAZ", "baz") 610 | os.Setenv("ENV_CONFIG_STRUCT", "inner") 611 | 612 | if err := Process("env_config", &s); err != nil { 613 | t.Error(err.Error()) 614 | } 615 | 616 | if want := "foo"; s.Foo != want { 617 | t.Errorf("foo: got %#q, want %#q", s.Foo, want) 618 | } 619 | 620 | if want := "[bar]"; s.Bar.String() != want { 621 | t.Errorf("bar: got %#q, want %#q", s.Bar, want) 622 | } 623 | 624 | if want := `["baz"]`; s.Baz.String() != want { 625 | t.Errorf(`baz: got %#q, want %#q`, s.Baz, want) 626 | } 627 | 628 | if want := `setterstruct{"inner"}`; s.Struct.Inner != want { 629 | t.Errorf(`Struct.Inner: got %#q, want %#q`, s.Struct.Inner, want) 630 | } 631 | } 632 | 633 | func TestCustomPointerFields(t *testing.T) { 634 | var s struct { 635 | Foo string 636 | Bar *bracketed 637 | Baz *quoted 638 | Struct *setterStruct 639 | } 640 | 641 | // Set would panic when the receiver is nil, 642 | // so make sure they have initial values to replace. 643 | s.Bar = new(bracketed) 644 | s.Baz = "ed{new(bracketed)} 645 | 646 | os.Clearenv() 647 | os.Setenv("ENV_CONFIG_FOO", "foo") 648 | os.Setenv("ENV_CONFIG_BAR", "bar") 649 | os.Setenv("ENV_CONFIG_BAZ", "baz") 650 | os.Setenv("ENV_CONFIG_STRUCT", "inner") 651 | 652 | if err := Process("env_config", &s); err != nil { 653 | t.Error(err.Error()) 654 | } 655 | 656 | if want := "foo"; s.Foo != want { 657 | t.Errorf("foo: got %#q, want %#q", s.Foo, want) 658 | } 659 | 660 | if want := "[bar]"; s.Bar.String() != want { 661 | t.Errorf("bar: got %#q, want %#q", s.Bar, want) 662 | } 663 | 664 | if want := `["baz"]`; s.Baz.String() != want { 665 | t.Errorf(`baz: got %#q, want %#q`, s.Baz, want) 666 | } 667 | 668 | if want := `setterstruct{"inner"}`; s.Struct.Inner != want { 669 | t.Errorf(`Struct.Inner: got %#q, want %#q`, s.Struct.Inner, want) 670 | } 671 | } 672 | 673 | func TestEmptyPrefixUsesFieldNames(t *testing.T) { 674 | var s Specification 675 | os.Clearenv() 676 | os.Setenv("REQUIREDVAR", "foo") 677 | 678 | err := Process("", &s) 679 | if err != nil { 680 | t.Errorf("Process failed: %s", err) 681 | } 682 | 683 | if s.RequiredVar != "foo" { 684 | t.Errorf( 685 | `RequiredVar not populated correctly: expected "foo", got %q`, 686 | s.RequiredVar, 687 | ) 688 | } 689 | } 690 | 691 | func TestNestedStructVarName(t *testing.T) { 692 | var s Specification 693 | os.Clearenv() 694 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "required") 695 | val := "found with only short name" 696 | os.Setenv("INNER", val) 697 | if err := Process("env_config", &s); err != nil { 698 | t.Error(err.Error()) 699 | } 700 | if s.NestedSpecification.Property != val { 701 | t.Errorf("expected %s, got %s", val, s.NestedSpecification.Property) 702 | } 703 | } 704 | 705 | func TestTextUnmarshalerError(t *testing.T) { 706 | var s Specification 707 | os.Clearenv() 708 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 709 | os.Setenv("ENV_CONFIG_DATETIME", "I'M NOT A DATE") 710 | 711 | err := Process("env_config", &s) 712 | 713 | v, ok := err.(*ParseError) 714 | if !ok { 715 | t.Errorf("expected ParseError, got %v", v) 716 | } 717 | if v.FieldName != "Datetime" { 718 | t.Errorf("expected %s, got %v", "Datetime", v.FieldName) 719 | } 720 | 721 | expectedLowLevelError := time.ParseError{ 722 | Layout: time.RFC3339, 723 | Value: "I'M NOT A DATE", 724 | LayoutElem: "2006", 725 | ValueElem: "I'M NOT A DATE", 726 | } 727 | 728 | if v.Err.Error() != expectedLowLevelError.Error() { 729 | t.Errorf("expected %s, got %s", expectedLowLevelError, v.Err) 730 | } 731 | } 732 | 733 | func TestBinaryUnmarshalerError(t *testing.T) { 734 | var s Specification 735 | os.Clearenv() 736 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 737 | os.Setenv("ENV_CONFIG_URLPOINTER", "http://%41:8080/") 738 | 739 | err := Process("env_config", &s) 740 | 741 | v, ok := err.(*ParseError) 742 | if !ok { 743 | t.Fatalf("expected ParseError, got %T %v", err, err) 744 | } 745 | if v.FieldName != "UrlPointer" { 746 | t.Errorf("expected %s, got %v", "UrlPointer", v.FieldName) 747 | } 748 | 749 | // To be compatible with go 1.5 and lower we should do a very basic check, 750 | // because underlying error message varies in go 1.5 and go 1.6+. 751 | 752 | ue, ok := v.Err.(*url.Error) 753 | if !ok { 754 | t.Errorf("expected error type to be \"*url.Error\", got %T", v.Err) 755 | } 756 | 757 | if ue.Op != "parse" { 758 | t.Errorf("expected error op to be \"parse\", got %q", ue.Op) 759 | } 760 | } 761 | 762 | func TestCheckDisallowedOnlyAllowed(t *testing.T) { 763 | var s Specification 764 | os.Clearenv() 765 | os.Setenv("ENV_CONFIG_DEBUG", "true") 766 | os.Setenv("UNRELATED_ENV_VAR", "true") 767 | err := CheckDisallowed("env_config", &s) 768 | if err != nil { 769 | t.Errorf("expected no error, got %s", err) 770 | } 771 | } 772 | 773 | func TestCheckDisallowedMispelled(t *testing.T) { 774 | var s Specification 775 | os.Clearenv() 776 | os.Setenv("ENV_CONFIG_DEBUG", "true") 777 | os.Setenv("ENV_CONFIG_ZEBUG", "false") 778 | err := CheckDisallowed("env_config", &s) 779 | if experr := "unknown environment variable ENV_CONFIG_ZEBUG"; err.Error() != experr { 780 | t.Errorf("expected %s, got %s", experr, err) 781 | } 782 | } 783 | 784 | func TestCheckDisallowedIgnored(t *testing.T) { 785 | var s Specification 786 | os.Clearenv() 787 | os.Setenv("ENV_CONFIG_DEBUG", "true") 788 | os.Setenv("ENV_CONFIG_IGNORED", "false") 789 | err := CheckDisallowed("env_config", &s) 790 | if experr := "unknown environment variable ENV_CONFIG_IGNORED"; err.Error() != experr { 791 | t.Errorf("expected %s, got %s", experr, err) 792 | } 793 | } 794 | 795 | func TestErrorMessageForRequiredAltVar(t *testing.T) { 796 | var s struct { 797 | Foo string `envconfig:"BAR" required:"true"` 798 | } 799 | 800 | os.Clearenv() 801 | err := Process("env_config", &s) 802 | 803 | if err == nil { 804 | t.Error("no failure when missing required variable") 805 | } 806 | 807 | if !strings.Contains(err.Error(), " BAR ") { 808 | t.Errorf("expected error message to contain BAR, got \"%v\"", err) 809 | } 810 | } 811 | 812 | type bracketed string 813 | 814 | func (b *bracketed) Set(value string) error { 815 | *b = bracketed("[" + value + "]") 816 | return nil 817 | } 818 | 819 | func (b bracketed) String() string { 820 | return string(b) 821 | } 822 | 823 | // quoted is used to test the precedence of Decode over Set. 824 | // The sole field is a flag.Value rather than a setter to validate that 825 | // all flag.Value implementations are also Setter implementations. 826 | type quoted struct{ flag.Value } 827 | 828 | func (d quoted) Decode(value string) error { 829 | return d.Set(`"` + value + `"`) 830 | } 831 | 832 | type setterStruct struct { 833 | Inner string 834 | } 835 | 836 | func (ss *setterStruct) Set(value string) error { 837 | ss.Inner = fmt.Sprintf("setterstruct{%q}", value) 838 | return nil 839 | } 840 | 841 | func BenchmarkGatherInfo(b *testing.B) { 842 | os.Clearenv() 843 | os.Setenv("ENV_CONFIG_DEBUG", "true") 844 | os.Setenv("ENV_CONFIG_PORT", "8080") 845 | os.Setenv("ENV_CONFIG_RATE", "0.5") 846 | os.Setenv("ENV_CONFIG_USER", "Kelsey") 847 | os.Setenv("ENV_CONFIG_TIMEOUT", "2m") 848 | os.Setenv("ENV_CONFIG_ADMINUSERS", "John,Adam,Will") 849 | os.Setenv("ENV_CONFIG_MAGICNUMBERS", "5,10,20") 850 | os.Setenv("ENV_CONFIG_COLORCODES", "red:1,green:2,blue:3") 851 | os.Setenv("SERVICE_HOST", "127.0.0.1") 852 | os.Setenv("ENV_CONFIG_TTL", "30") 853 | os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") 854 | os.Setenv("ENV_CONFIG_IGNORED", "was-not-ignored") 855 | os.Setenv("ENV_CONFIG_OUTER_INNER", "iamnested") 856 | os.Setenv("ENV_CONFIG_AFTERNESTED", "after") 857 | os.Setenv("ENV_CONFIG_HONOR", "honor") 858 | os.Setenv("ENV_CONFIG_DATETIME", "2016-08-16T18:57:05Z") 859 | os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT", "24") 860 | for i := 0; i < b.N; i++ { 861 | var s Specification 862 | gatherInfo("env_config", &s) 863 | } 864 | } 865 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kelseyhightower/envconfig 2 | -------------------------------------------------------------------------------- /testdata/custom.txt: -------------------------------------------------------------------------------- 1 | ENV_CONFIG_ENABLED=some.embedded.value 2 | ENV_CONFIG_EMBEDDEDPORT= 3 | ENV_CONFIG_MULTIWORDVAR= 4 | ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT= 5 | ENV_CONFIG_EMBEDDED_WITH_ALT= 6 | ENV_CONFIG_DEBUG= 7 | ENV_CONFIG_PORT= 8 | ENV_CONFIG_RATE= 9 | ENV_CONFIG_USER= 10 | ENV_CONFIG_TTL= 11 | ENV_CONFIG_TIMEOUT= 12 | ENV_CONFIG_ADMINUSERS= 13 | ENV_CONFIG_MAGICNUMBERS= 14 | ENV_CONFIG_EMPTYNUMBERS= 15 | ENV_CONFIG_BYTESLICE= 16 | ENV_CONFIG_COLORCODES= 17 | ENV_CONFIG_MULTIWORDVAR= 18 | ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT= 19 | ENV_CONFIG_MULTI_WORD_ACR_WITH_AUTO_SPLIT= 20 | ENV_CONFIG_SOMEPOINTER= 21 | ENV_CONFIG_SOMEPOINTERWITHDEFAULT=foorbar.is.the.word 22 | ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT=what.alt 23 | ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT= 24 | ENV_CONFIG_SERVICE_HOST= 25 | ENV_CONFIG_DEFAULTVAR= 26 | ENV_CONFIG_REQUIREDVAR= 27 | ENV_CONFIG_BROKER= 28 | ENV_CONFIG_REQUIREDDEFAULT= 29 | ENV_CONFIG_OUTER_INNER= 30 | ENV_CONFIG_OUTER_PROPERTYWITHDEFAULT= 31 | ENV_CONFIG_AFTERNESTED= 32 | ENV_CONFIG_HONOR= 33 | ENV_CONFIG_DATETIME= 34 | ENV_CONFIG_MAPFIELD= 35 | ENV_CONFIG_URLVALUE= 36 | ENV_CONFIG_URLPOINTER= 37 | -------------------------------------------------------------------------------- /testdata/default_list.txt: -------------------------------------------------------------------------------- 1 | This.application.is.configured.via.the.environment..The.following.environment 2 | variables.can.be.used: 3 | 4 | ENV_CONFIG_ENABLED 5 | ..[description].some.embedded.value 6 | ..[type]........True.or.False 7 | ..[default]..... 8 | ..[required].... 9 | ENV_CONFIG_EMBEDDEDPORT 10 | ..[description]. 11 | ..[type]........Integer 12 | ..[default]..... 13 | ..[required].... 14 | ENV_CONFIG_MULTIWORDVAR 15 | ..[description]. 16 | ..[type]........String 17 | ..[default]..... 18 | ..[required].... 19 | ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT 20 | ..[description]. 21 | ..[type]........String 22 | ..[default]..... 23 | ..[required].... 24 | ENV_CONFIG_EMBEDDED_WITH_ALT 25 | ..[description]. 26 | ..[type]........String 27 | ..[default]..... 28 | ..[required].... 29 | ENV_CONFIG_DEBUG 30 | ..[description]. 31 | ..[type]........True.or.False 32 | ..[default]..... 33 | ..[required].... 34 | ENV_CONFIG_PORT 35 | ..[description]. 36 | ..[type]........Integer 37 | ..[default]..... 38 | ..[required].... 39 | ENV_CONFIG_RATE 40 | ..[description]. 41 | ..[type]........Float 42 | ..[default]..... 43 | ..[required].... 44 | ENV_CONFIG_USER 45 | ..[description]. 46 | ..[type]........String 47 | ..[default]..... 48 | ..[required].... 49 | ENV_CONFIG_TTL 50 | ..[description]. 51 | ..[type]........Unsigned.Integer 52 | ..[default]..... 53 | ..[required].... 54 | ENV_CONFIG_TIMEOUT 55 | ..[description]. 56 | ..[type]........Duration 57 | ..[default]..... 58 | ..[required].... 59 | ENV_CONFIG_ADMINUSERS 60 | ..[description]. 61 | ..[type]........Comma-separated.list.of.String 62 | ..[default]..... 63 | ..[required].... 64 | ENV_CONFIG_MAGICNUMBERS 65 | ..[description]. 66 | ..[type]........Comma-separated.list.of.Integer 67 | ..[default]..... 68 | ..[required].... 69 | ENV_CONFIG_EMPTYNUMBERS 70 | ..[description]. 71 | ..[type]........Comma-separated.list.of.Integer 72 | ..[default]..... 73 | ..[required].... 74 | ENV_CONFIG_BYTESLICE 75 | ..[description]. 76 | ..[type]........String 77 | ..[default]..... 78 | ..[required].... 79 | ENV_CONFIG_COLORCODES 80 | ..[description]. 81 | ..[type]........Comma-separated.list.of.String:Integer.pairs 82 | ..[default]..... 83 | ..[required].... 84 | ENV_CONFIG_MULTIWORDVAR 85 | ..[description]. 86 | ..[type]........String 87 | ..[default]..... 88 | ..[required].... 89 | ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT 90 | ..[description]. 91 | ..[type]........Unsigned.Integer 92 | ..[default]..... 93 | ..[required].... 94 | ENV_CONFIG_MULTI_WORD_ACR_WITH_AUTO_SPLIT 95 | ..[description]. 96 | ..[type]........Unsigned.Integer 97 | ..[default]..... 98 | ..[required].... 99 | ENV_CONFIG_SOMEPOINTER 100 | ..[description]. 101 | ..[type]........String 102 | ..[default]..... 103 | ..[required].... 104 | ENV_CONFIG_SOMEPOINTERWITHDEFAULT 105 | ..[description].foorbar.is.the.word 106 | ..[type]........String 107 | ..[default].....foo2baz 108 | ..[required].... 109 | ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT 110 | ..[description].what.alt 111 | ..[type]........String 112 | ..[default]..... 113 | ..[required].... 114 | ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT 115 | ..[description]. 116 | ..[type]........String 117 | ..[default]..... 118 | ..[required].... 119 | ENV_CONFIG_SERVICE_HOST 120 | ..[description]. 121 | ..[type]........String 122 | ..[default]..... 123 | ..[required].... 124 | ENV_CONFIG_DEFAULTVAR 125 | ..[description]. 126 | ..[type]........String 127 | ..[default].....foobar 128 | ..[required].... 129 | ENV_CONFIG_REQUIREDVAR 130 | ..[description]. 131 | ..[type]........String 132 | ..[default]..... 133 | ..[required]....true 134 | ENV_CONFIG_BROKER 135 | ..[description]. 136 | ..[type]........String 137 | ..[default].....127.0.0.1 138 | ..[required].... 139 | ENV_CONFIG_REQUIREDDEFAULT 140 | ..[description]. 141 | ..[type]........String 142 | ..[default].....foo2bar 143 | ..[required]....true 144 | ENV_CONFIG_OUTER_INNER 145 | ..[description]. 146 | ..[type]........String 147 | ..[default]..... 148 | ..[required].... 149 | ENV_CONFIG_OUTER_PROPERTYWITHDEFAULT 150 | ..[description]. 151 | ..[type]........String 152 | ..[default].....fuzzybydefault 153 | ..[required].... 154 | ENV_CONFIG_AFTERNESTED 155 | ..[description]. 156 | ..[type]........String 157 | ..[default]..... 158 | ..[required].... 159 | ENV_CONFIG_HONOR 160 | ..[description]. 161 | ..[type]........HonorDecodeInStruct 162 | ..[default]..... 163 | ..[required].... 164 | ENV_CONFIG_DATETIME 165 | ..[description]. 166 | ..[type]........Time 167 | ..[default]..... 168 | ..[required].... 169 | ENV_CONFIG_MAPFIELD 170 | ..[description]. 171 | ..[type]........Comma-separated.list.of.String:String.pairs 172 | ..[default].....one:two,three:four 173 | ..[required].... 174 | ENV_CONFIG_URLVALUE 175 | ..[description]. 176 | ..[type]........CustomURL 177 | ..[default]..... 178 | ..[required].... 179 | ENV_CONFIG_URLPOINTER 180 | ..[description]. 181 | ..[type]........CustomURL 182 | ..[default]..... 183 | ..[required].... 184 | -------------------------------------------------------------------------------- /testdata/default_table.txt: -------------------------------------------------------------------------------- 1 | This.application.is.configured.via.the.environment..The.following.environment 2 | variables.can.be.used: 3 | 4 | KEY..............................................TYPE............................................DEFAULT...............REQUIRED....DESCRIPTION 5 | ENV_CONFIG_ENABLED...............................True.or.False.....................................................................some.embedded.value 6 | ENV_CONFIG_EMBEDDEDPORT..........................Integer........................................................................... 7 | ENV_CONFIG_MULTIWORDVAR..........................String............................................................................ 8 | ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT..............String............................................................................ 9 | ENV_CONFIG_EMBEDDED_WITH_ALT.....................String............................................................................ 10 | ENV_CONFIG_DEBUG.................................True.or.False..................................................................... 11 | ENV_CONFIG_PORT..................................Integer........................................................................... 12 | ENV_CONFIG_RATE..................................Float............................................................................. 13 | ENV_CONFIG_USER..................................String............................................................................ 14 | ENV_CONFIG_TTL...................................Unsigned.Integer.................................................................. 15 | ENV_CONFIG_TIMEOUT...............................Duration.......................................................................... 16 | ENV_CONFIG_ADMINUSERS............................Comma-separated.list.of.String.................................................... 17 | ENV_CONFIG_MAGICNUMBERS..........................Comma-separated.list.of.Integer................................................... 18 | ENV_CONFIG_EMPTYNUMBERS..........................Comma-separated.list.of.Integer................................................... 19 | ENV_CONFIG_BYTESLICE.............................String............................................................................ 20 | ENV_CONFIG_COLORCODES............................Comma-separated.list.of.String:Integer.pairs...................................... 21 | ENV_CONFIG_MULTIWORDVAR..........................String............................................................................ 22 | ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT........Unsigned.Integer.................................................................. 23 | ENV_CONFIG_MULTI_WORD_ACR_WITH_AUTO_SPLIT........Unsigned.Integer.................................................................. 24 | ENV_CONFIG_SOMEPOINTER...........................String............................................................................ 25 | ENV_CONFIG_SOMEPOINTERWITHDEFAULT................String..........................................foo2baz...........................foorbar.is.the.word 26 | ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT...............String............................................................................what.alt 27 | ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT....String............................................................................ 28 | ENV_CONFIG_SERVICE_HOST..........................String............................................................................ 29 | ENV_CONFIG_DEFAULTVAR............................String..........................................foobar............................ 30 | ENV_CONFIG_REQUIREDVAR...........................String................................................................true........ 31 | ENV_CONFIG_BROKER................................String..........................................127.0.0.1......................... 32 | ENV_CONFIG_REQUIREDDEFAULT.......................String..........................................foo2bar...............true........ 33 | ENV_CONFIG_OUTER_INNER...........................String............................................................................ 34 | ENV_CONFIG_OUTER_PROPERTYWITHDEFAULT.............String..........................................fuzzybydefault.................... 35 | ENV_CONFIG_AFTERNESTED...........................String............................................................................ 36 | ENV_CONFIG_HONOR.................................HonorDecodeInStruct............................................................... 37 | ENV_CONFIG_DATETIME..............................Time.............................................................................. 38 | ENV_CONFIG_MAPFIELD..............................Comma-separated.list.of.String:String.pairs.....one:two,three:four................ 39 | ENV_CONFIG_URLVALUE..............................CustomURL......................................................................... 40 | ENV_CONFIG_URLPOINTER............................CustomURL......................................................................... 41 | -------------------------------------------------------------------------------- /testdata/fault.txt: -------------------------------------------------------------------------------- 1 | {.Key} 2 | {.Key} 3 | {.Key} 4 | {.Key} 5 | {.Key} 6 | {.Key} 7 | {.Key} 8 | {.Key} 9 | {.Key} 10 | {.Key} 11 | {.Key} 12 | {.Key} 13 | {.Key} 14 | {.Key} 15 | {.Key} 16 | {.Key} 17 | {.Key} 18 | {.Key} 19 | {.Key} 20 | {.Key} 21 | {.Key} 22 | {.Key} 23 | {.Key} 24 | {.Key} 25 | {.Key} 26 | {.Key} 27 | {.Key} 28 | {.Key} 29 | {.Key} 30 | {.Key} 31 | {.Key} 32 | {.Key} 33 | {.Key} 34 | {.Key} 35 | {.Key} 36 | {.Key} 37 | -------------------------------------------------------------------------------- /usage.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Kelsey Hightower and others. All rights reserved. 2 | // Use of this source code is governed by the MIT License that can be found in 3 | // the LICENSE file. 4 | 5 | package envconfig 6 | 7 | import ( 8 | "encoding" 9 | "fmt" 10 | "io" 11 | "os" 12 | "reflect" 13 | "strconv" 14 | "strings" 15 | "text/tabwriter" 16 | "text/template" 17 | ) 18 | 19 | const ( 20 | // DefaultListFormat constant to use to display usage in a list format 21 | DefaultListFormat = `This application is configured via the environment. The following environment 22 | variables can be used: 23 | {{range .}} 24 | {{usage_key .}} 25 | [description] {{usage_description .}} 26 | [type] {{usage_type .}} 27 | [default] {{usage_default .}} 28 | [required] {{usage_required .}}{{end}} 29 | ` 30 | // DefaultTableFormat constant to use to display usage in a tabular format 31 | DefaultTableFormat = `This application is configured via the environment. The following environment 32 | variables can be used: 33 | 34 | KEY TYPE DEFAULT REQUIRED DESCRIPTION 35 | {{range .}}{{usage_key .}} {{usage_type .}} {{usage_default .}} {{usage_required .}} {{usage_description .}} 36 | {{end}}` 37 | ) 38 | 39 | var ( 40 | decoderType = reflect.TypeOf((*Decoder)(nil)).Elem() 41 | setterType = reflect.TypeOf((*Setter)(nil)).Elem() 42 | textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() 43 | binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem() 44 | ) 45 | 46 | func implementsInterface(t reflect.Type) bool { 47 | return t.Implements(decoderType) || 48 | reflect.PtrTo(t).Implements(decoderType) || 49 | t.Implements(setterType) || 50 | reflect.PtrTo(t).Implements(setterType) || 51 | t.Implements(textUnmarshalerType) || 52 | reflect.PtrTo(t).Implements(textUnmarshalerType) || 53 | t.Implements(binaryUnmarshalerType) || 54 | reflect.PtrTo(t).Implements(binaryUnmarshalerType) 55 | } 56 | 57 | // toTypeDescription converts Go types into a human readable description 58 | func toTypeDescription(t reflect.Type) string { 59 | switch t.Kind() { 60 | case reflect.Array, reflect.Slice: 61 | if t.Elem().Kind() == reflect.Uint8 { 62 | return "String" 63 | } 64 | return fmt.Sprintf("Comma-separated list of %s", toTypeDescription(t.Elem())) 65 | case reflect.Map: 66 | return fmt.Sprintf( 67 | "Comma-separated list of %s:%s pairs", 68 | toTypeDescription(t.Key()), 69 | toTypeDescription(t.Elem()), 70 | ) 71 | case reflect.Ptr: 72 | return toTypeDescription(t.Elem()) 73 | case reflect.Struct: 74 | if implementsInterface(t) && t.Name() != "" { 75 | return t.Name() 76 | } 77 | return "" 78 | case reflect.String: 79 | name := t.Name() 80 | if name != "" && name != "string" { 81 | return name 82 | } 83 | return "String" 84 | case reflect.Bool: 85 | name := t.Name() 86 | if name != "" && name != "bool" { 87 | return name 88 | } 89 | return "True or False" 90 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 91 | name := t.Name() 92 | if name != "" && !strings.HasPrefix(name, "int") { 93 | return name 94 | } 95 | return "Integer" 96 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 97 | name := t.Name() 98 | if name != "" && !strings.HasPrefix(name, "uint") { 99 | return name 100 | } 101 | return "Unsigned Integer" 102 | case reflect.Float32, reflect.Float64: 103 | name := t.Name() 104 | if name != "" && !strings.HasPrefix(name, "float") { 105 | return name 106 | } 107 | return "Float" 108 | } 109 | return fmt.Sprintf("%+v", t) 110 | } 111 | 112 | // Usage writes usage information to stdout using the default header and table format 113 | func Usage(prefix string, spec interface{}) error { 114 | // The default is to output the usage information as a table 115 | // Create tabwriter instance to support table output 116 | tabs := tabwriter.NewWriter(os.Stdout, 1, 0, 4, ' ', 0) 117 | 118 | err := Usagef(prefix, spec, tabs, DefaultTableFormat) 119 | tabs.Flush() 120 | return err 121 | } 122 | 123 | // Usagef writes usage information to the specified io.Writer using the specified template specification 124 | func Usagef(prefix string, spec interface{}, out io.Writer, format string) error { 125 | 126 | // Specify the default usage template functions 127 | functions := template.FuncMap{ 128 | "usage_key": func(v varInfo) string { return v.Key }, 129 | "usage_description": func(v varInfo) string { return v.Tags.Get("desc") }, 130 | "usage_type": func(v varInfo) string { return toTypeDescription(v.Field.Type()) }, 131 | "usage_default": func(v varInfo) string { return v.Tags.Get("default") }, 132 | "usage_required": func(v varInfo) (string, error) { 133 | req := v.Tags.Get("required") 134 | if req != "" { 135 | reqB, err := strconv.ParseBool(req) 136 | if err != nil { 137 | return "", err 138 | } 139 | if reqB { 140 | req = "true" 141 | } 142 | } 143 | return req, nil 144 | }, 145 | } 146 | 147 | tmpl, err := template.New("envconfig").Funcs(functions).Parse(format) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | return Usaget(prefix, spec, out, tmpl) 153 | } 154 | 155 | // Usaget writes usage information to the specified io.Writer using the specified template 156 | func Usaget(prefix string, spec interface{}, out io.Writer, tmpl *template.Template) error { 157 | // gather first 158 | infos, err := gatherInfo(prefix, spec) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | return tmpl.Execute(out, infos) 164 | } 165 | -------------------------------------------------------------------------------- /usage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Kelsey Hightower and others. All rights reserved. 2 | // Use of this source code is governed by the MIT License that can be found in 3 | // the LICENSE file. 4 | 5 | package envconfig 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "strings" 14 | "testing" 15 | "text/tabwriter" 16 | ) 17 | 18 | var testUsageTableResult, testUsageListResult, testUsageCustomResult, testUsageBadFormatResult string 19 | 20 | func TestMain(m *testing.M) { 21 | 22 | // Load the expected test results from a text file 23 | data, err := ioutil.ReadFile("testdata/default_table.txt") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | testUsageTableResult = string(data) 28 | 29 | data, err = ioutil.ReadFile("testdata/default_list.txt") 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | testUsageListResult = string(data) 34 | 35 | data, err = ioutil.ReadFile("testdata/custom.txt") 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | testUsageCustomResult = string(data) 40 | 41 | data, err = ioutil.ReadFile("testdata/fault.txt") 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | testUsageBadFormatResult = string(data) 46 | 47 | retCode := m.Run() 48 | os.Exit(retCode) 49 | } 50 | 51 | func compareUsage(want, got string, t *testing.T) { 52 | got = strings.ReplaceAll(got, " ", ".") 53 | if want != got { 54 | shortest := len(want) 55 | if len(got) < shortest { 56 | shortest = len(got) 57 | } 58 | if len(want) != len(got) { 59 | t.Errorf("expected result length of %d, found %d", len(want), len(got)) 60 | } 61 | for i := 0; i < shortest; i++ { 62 | if want[i] != got[i] { 63 | t.Errorf("difference at index %d, expected '%c' (%v), found '%c' (%v)\n", 64 | i, want[i], want[i], got[i], got[i]) 65 | break 66 | } 67 | } 68 | t.Errorf("Complete Expected:\n'%s'\nComplete Found:\n'%s'\n", want, got) 69 | } 70 | } 71 | 72 | func TestUsageDefault(t *testing.T) { 73 | var s Specification 74 | os.Clearenv() 75 | save := os.Stdout 76 | r, w, _ := os.Pipe() 77 | os.Stdout = w 78 | err := Usage("env_config", &s) 79 | outC := make(chan string) 80 | // copy the output in a separate goroutine so printing can't block indefinitely 81 | go func() { 82 | var buf bytes.Buffer 83 | io.Copy(&buf, r) 84 | outC <- buf.String() 85 | }() 86 | w.Close() 87 | os.Stdout = save // restoring the real stdout 88 | out := <-outC 89 | 90 | if err != nil { 91 | t.Error(err.Error()) 92 | } 93 | compareUsage(testUsageTableResult, out, t) 94 | } 95 | 96 | func TestUsageTable(t *testing.T) { 97 | var s Specification 98 | os.Clearenv() 99 | buf := new(bytes.Buffer) 100 | tabs := tabwriter.NewWriter(buf, 1, 0, 4, ' ', 0) 101 | err := Usagef("env_config", &s, tabs, DefaultTableFormat) 102 | tabs.Flush() 103 | if err != nil { 104 | t.Error(err.Error()) 105 | } 106 | compareUsage(testUsageTableResult, buf.String(), t) 107 | } 108 | 109 | func TestUsageList(t *testing.T) { 110 | var s Specification 111 | os.Clearenv() 112 | buf := new(bytes.Buffer) 113 | err := Usagef("env_config", &s, buf, DefaultListFormat) 114 | if err != nil { 115 | t.Error(err.Error()) 116 | } 117 | compareUsage(testUsageListResult, buf.String(), t) 118 | } 119 | 120 | func TestUsageCustomFormat(t *testing.T) { 121 | var s Specification 122 | os.Clearenv() 123 | buf := new(bytes.Buffer) 124 | err := Usagef("env_config", &s, buf, "{{range .}}{{usage_key .}}={{usage_description .}}\n{{end}}") 125 | if err != nil { 126 | t.Error(err.Error()) 127 | } 128 | compareUsage(testUsageCustomResult, buf.String(), t) 129 | } 130 | 131 | func TestUsageUnknownKeyFormat(t *testing.T) { 132 | var s Specification 133 | unknownError := "template: envconfig:1:2: executing \"envconfig\" at <.UnknownKey>" 134 | os.Clearenv() 135 | buf := new(bytes.Buffer) 136 | err := Usagef("env_config", &s, buf, "{{.UnknownKey}}") 137 | if err == nil { 138 | t.Errorf("expected 'unknown key' error, but got no error") 139 | } 140 | if !strings.Contains(err.Error(), unknownError) { 141 | t.Errorf("expected '%s', but got '%s'", unknownError, err.Error()) 142 | } 143 | } 144 | 145 | func TestUsageBadFormat(t *testing.T) { 146 | var s Specification 147 | os.Clearenv() 148 | // If you don't use two {{}} then you get a lieteral 149 | buf := new(bytes.Buffer) 150 | err := Usagef("env_config", &s, buf, "{{range .}}{.Key}\n{{end}}") 151 | if err != nil { 152 | t.Error(err.Error()) 153 | } 154 | compareUsage(testUsageBadFormatResult, buf.String(), t) 155 | } 156 | --------------------------------------------------------------------------------