1414
1515// Package envconfig populates struct fields based on environment variable
1616// values (or anything that responds to "Lookup"). Structs declare their
17- // environment dependencies using the ` env` tag with the key being the name of
17+ // environment dependencies using the " env" tag with the key being the name of
1818// the environment variable, case sensitive.
1919//
20- // type MyStruct struct {
21- // A string `env:"A"` // resolves A to $A
22- // B string `env:"B,required"` // resolves B to $B, errors if $B is unset
23- // C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo"
20+ // type MyStruct struct {
21+ // A string `env:"A"` // resolves A to $A
22+ // B string `env:"B,required"` // resolves B to $B, errors if $B is unset
23+ // C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo"
2424//
25- // D string `env:"D,required,default=foo"` // error, cannot be required and default
26- // E string `env:""` // error, must specify key
27- // }
25+ // D string `env:"D,required,default=foo"` // error, cannot be required and default
26+ // E string `env:""` // error, must specify key
27+ // }
2828//
2929// All built-in types are supported except Func and Chan. If you need to define
3030// a custom decoder, implement Decoder:
3131//
32- // type MyStruct struct {
33- // field string
34- // }
32+ // type MyStruct struct {
33+ // field string
34+ // }
3535//
36- // func (v *MyStruct) EnvDecode(val string) error {
37- // v.field = fmt.Sprintf("PREFIX-%s", val)
38- // return nil
39- // }
36+ // func (v *MyStruct) EnvDecode(val string) error {
37+ // v.field = fmt.Sprintf("PREFIX-%s", val)
38+ // return nil
39+ // }
4040//
4141// In the environment, slices are specified as comma-separated values:
4242//
43- // export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"}
43+ // export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"}
4444//
4545// In the environment, maps are specified as comma-separated key:value pairs:
4646//
47- // export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"}
47+ // export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"}
4848//
4949// If you need to modify environment variable values before processing, you can
5050// specify a custom mutator:
5151//
52- // type Config struct {
53- // Password `env:"PASSWORD_SECRET"`
54- // }
52+ // type Config struct {
53+ // Password `env:"PASSWORD_SECRET"`
54+ // }
5555//
56- // func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
57- // if strings.HasPrefix(value, "secret://") {
58- // return secretmanager.Resolve(ctx, value) // example
59- // }
60- // return value, nil
61- // }
62- //
63- // var config Config
64- // ProcessWith(&config, OsLookuper(), resolveSecretFunc)
56+ // func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
57+ // if strings.HasPrefix(value, "secret://") {
58+ // return secretmanager.Resolve(ctx, value) // example
59+ // }
60+ // return value, nil
61+ // }
6562//
63+ // var config Config
64+ // ProcessWith(&config, OsLookuper(), resolveSecretFunc)
6665package envconfig
6766
6867import (
@@ -74,7 +73,6 @@ import (
7473 "fmt"
7574 "os"
7675 "reflect"
77- "regexp"
7876 "strconv"
7977 "strings"
8078 "time"
@@ -95,8 +93,6 @@ const (
9593 defaultSeparator = ":"
9694)
9795
98- var envvarNameRe = regexp .MustCompile (`\A[a-zA-Z_][a-zA-Z0-9_]*\z` )
99-
10096// Error is a custom error type for errors returned by envconfig.
10197type Error string
10298
@@ -138,7 +134,7 @@ func (o *osLookuper) Lookup(key string) (string, bool) {
138134 return os .LookupEnv (key )
139135}
140136
141- // OsLookuper returns a lookuper that uses the environment (os.LookupEnv) to
137+ // OsLookuper returns a lookuper that uses the environment ([ os.LookupEnv] ) to
142138// resolve values.
143139func OsLookuper () Lookuper {
144140 return new (osLookuper )
@@ -203,12 +199,11 @@ func MultiLookuper(lookupers ...Lookuper) Lookuper {
203199// Decoder is an interface that custom types/fields can implement to control how
204200// decoding takes place. For example:
205201//
206- // type MyType string
207- //
208- // func (mt MyType) EnvDecode(val string) error {
209- // return "CUSTOM-"+val
210- // }
202+ // type MyType string
211203//
204+ // func (mt MyType) EnvDecode(val string) error {
205+ // return "CUSTOM-"+val
206+ // }
212207type Decoder interface {
213208 EnvDecode (val string ) error
214209}
@@ -229,7 +224,7 @@ type options struct {
229224 Required bool
230225}
231226
232- // Process processes the struct using the environment. See ProcessWith for a
227+ // Process processes the struct using the environment. See [ ProcessWith] for a
233228// more customizable version.
234229func Process (ctx context.Context , i interface {}) error {
235230 return ProcessWith (ctx , i , OsLookuper ())
@@ -427,7 +422,7 @@ func keyAndOpts(tag string) (string, *options, error) {
427422 parts := strings .Split (tag , "," )
428423 key , tagOpts := strings .TrimSpace (parts [0 ]), parts [1 :]
429424
430- if key != "" && ! envvarNameRe . MatchString (key ) {
425+ if key != "" && ! validateEnvName (key ) {
431426 return "" , nil , fmt .Errorf ("%q: %w " , key , ErrInvalidEnvvarName )
432427 }
433428
@@ -689,3 +684,34 @@ func processField(v string, ef reflect.Value, delimiter, separator string, noIni
689684
690685 return nil
691686}
687+
688+ // validateEnvName validates the given string conforms to being a valid
689+ // environment variable.
690+ //
691+ // Per IEEE Std 1003.1-2001 environment variables consist solely of uppercase
692+ // letters, digits, and _, and do not begin with a digit.
693+ func validateEnvName (s string ) bool {
694+ if s == "" {
695+ return false
696+ }
697+
698+ for i , r := range s {
699+ if (i == 0 && ! isLetter (r )) || (! isLetter (r ) && ! isNumber (r ) && r != '_' ) {
700+ return false
701+ }
702+ }
703+
704+ return true
705+ }
706+
707+ // isLetter returns true if the given rune is a letter between a-z,A-Z. This is
708+ // different than unicode.IsLetter which includes all L character cases.
709+ func isLetter (r rune ) bool {
710+ return (r >= 'a' && r <= 'z' ) || (r >= 'A' && r <= 'Z' )
711+ }
712+
713+ // isNumber returns true if the given run is a number between 0-9. This is
714+ // different than unicode.IsNumber in that it only allows 0-9.
715+ func isNumber (r rune ) bool {
716+ return r >= '0' && r <= '9'
717+ }
0 commit comments