// Copyright The envconfig Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package envconfig populates struct fields based on environment variable // values (or anything that responds to "Lookup"). Structs declare their // environment dependencies using the "env" tag with the key being the name of // the environment variable, case sensitive. // // type MyStruct struct { // A string `env:"A"` // resolves A to $A // B string `env:"B,required"` // resolves B to $B, errors if $B is unset // C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo" // // D string `env:"D,required,default=foo"` // error, cannot be required and default // E string `env:""` // error, must specify key // } // // All built-in types are supported except Func and Chan. If you need to define // a custom decoder, implement Decoder: // // type MyStruct struct { // field string // } // // func (v *MyStruct) EnvDecode(val string) error { // v.field = fmt.Sprintf("PREFIX-%s", val) // return nil // } // // In the environment, slices are specified as comma-separated values: // // export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"} // // In the environment, maps are specified as comma-separated key:value pairs: // // export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"} // // For more configuration options and examples, see the documentation.
package envconfig import ( ) const ( envTag = "env" optDecodeUnset = "decodeunset" optDefault = "default=" optDelimiter = "delimiter=" optNoInit = "noinit" optOverwrite = "overwrite" optPrefix = "prefix=" optRequired = "required" optSeparator = "separator=" ) // internalError is a custom error type for errors returned by envconfig. type internalError string // Error implements error. func ( internalError) () string { return string() } const ( ErrInvalidEnvvarName = internalError("invalid environment variable name") ErrInvalidMapItem = internalError("invalid map item") ErrLookuperNil = internalError("lookuper cannot be nil") ErrMissingKey = internalError("missing key") ErrMissingRequired = internalError("missing required value") ErrNoInitNotPtr = internalError("field must be a pointer to have noinit") ErrNotPtr = internalError("input must be a pointer") ErrNotStruct = internalError("input must be a struct") ErrPrefixNotStruct = internalError("prefix is only valid on struct types") ErrPrivateField = internalError("cannot parse private fields") ErrRequiredAndDefault = internalError("field cannot be required and have a default value") ErrUnknownOption = internalError("unknown option") ) // Lookuper is an interface that provides a lookup for a string-based key. type Lookuper interface { // Lookup searches for the given key and returns the corresponding string // value. If a value is found, it returns the value and true. If a value is // not found, it returns the empty string and false. Lookup(key string) (string, bool) } // osLookuper looks up environment configuration from the local environment. type osLookuper struct{} // Verify implements interface. var _ Lookuper = (*osLookuper)(nil) func ( *osLookuper) ( string) (string, bool) { return os.LookupEnv() } // OsLookuper returns a lookuper that uses the environment ([os.LookupEnv]) to // resolve values. func () Lookuper { return new(osLookuper) } type mapLookuper map[string]string var _ Lookuper = (*mapLookuper)(nil) func ( mapLookuper) ( string) (string, bool) { , := [] return , } // MapLookuper looks up environment configuration from a provided map. This is // useful for testing, especially in parallel, since it does not require you to // mutate the parent environment (which is stateful). func ( map[string]string) Lookuper { return mapLookuper() } type multiLookuper struct { ls []Lookuper } var _ Lookuper = (*multiLookuper)(nil) func ( *multiLookuper) ( string) (string, bool) { for , := range .ls { if , := .Lookup(); { return , true } } return "", false } // PrefixLookuper looks up environment configuration using the specified prefix. // This is useful if you want all your variables to start with a particular // prefix like "MY_APP_". func ( string, Lookuper) Lookuper { if , := .(*prefixLookuper); { return &prefixLookuper{prefix: .prefix + , l: .l} } return &prefixLookuper{prefix: , l: } } type prefixLookuper struct { l Lookuper prefix string } func ( *prefixLookuper) ( string) (string, bool) { return .l.Lookup(.Key()) } func ( *prefixLookuper) ( string) string { return .prefix + } func ( *prefixLookuper) () Lookuper { := .l for , := .(unwrappableLookuper); ; { = .Unwrap() } return } // unwrappableLookuper is a lookuper that can return the underlying lookuper. type unwrappableLookuper interface { Unwrap() Lookuper } // MultiLookuper wraps a collection of lookupers. It does not combine them, and // lookups appear in the order in which they are provided to the initializer. func ( ...Lookuper) Lookuper { return &multiLookuper{ls: } } // keyedLookuper is an extension to the [Lookuper] interface that returns the // underlying key (used by the [PrefixLookuper] or custom implementations). type keyedLookuper interface { Key(key string) string } // Decoder is an interface that custom types/fields can implement to control how // decoding takes place. For example: // // type MyType string // // func (mt MyType) EnvDecode(val string) error { // return "CUSTOM-"+val // } type Decoder interface { EnvDecode(val string) error } // options are internal options for decoding. type options struct { Default string Delimiter string Prefix string Separator string NoInit bool Overwrite bool DecodeUnset bool Required bool } // Config represent inputs to the envconfig decoding. type Config struct { // Target is the destination structure to decode. This value is required, and // it must be a pointer to a struct. Target any // Lookuper is the lookuper implementation to use. If not provided, it // defaults to the OS Lookuper. Lookuper Lookuper // DefaultDelimiter is the default value to use for the delimiter in maps and // slices. This can be overridden on a per-field basis, which takes // precedence. The default value is ",". DefaultDelimiter string // DefaultSeparator is the default value to use for the separator in maps. // This can be overridden on a per-field basis, which takes precedence. The // default value is ":". DefaultSeparator string // DefaultNoInit is the default value for skipping initialization of // unprovided fields. The default value is false (deeply initialize all // fields and nested structs). DefaultNoInit bool // DefaultOverwrite is the default value for overwriting an existing value set // on the struct before processing. The default value is false. DefaultOverwrite bool // DefaultDecodeUnset is the default value for running decoders even when no // value was given for the environment variable. DefaultDecodeUnset bool // DefaultRequired is the default value for marking a field as required. The // default value is false. DefaultRequired bool // Mutators is an optional list of mutators to apply to lookups. Mutators []Mutator } // Process decodes the struct using values from environment variables. See // [ProcessWith] for a more customizable version. // // As a special case, if the input for the target is a [*Config], then this // function will call [ProcessWith] using the provided config, with any mutation // appended. func ( context.Context, any, ...Mutator) error { if , := .(*Config); { .Mutators = append(.Mutators, ...) return ProcessWith(, ) } return ProcessWith(, &Config{ Target: , Mutators: , }) } // MustProcess is a helper that calls [Process] and panics if an error is // encountered. Unlike [Process], the input value is returned, making it ideal // for anonymous initializations: // // var env = envconfig.MustProcess(context.Background(), &struct{ // Field string `env:"FIELD,required"` // }) // // This is not recommend for production services, but it can be useful for quick // CLIs and scripts that want to take advantage of envconfig's environment // parsing at the expense of testability and graceful error handling. func [ any]( context.Context, , ...Mutator) { if := Process(, , ...); != nil { panic() } return } // ProcessWith executes the decoding process using the provided [Config]. func ( context.Context, *Config) error { if == nil { = new(Config) } if .Lookuper == nil { .Lookuper = OsLookuper() } // Deep copy the slice and remove any nil mutators. var []Mutator for , := range .Mutators { if != nil { = append(, ) } } .Mutators = return processWith(, ) } // processWith is a helper that retains configuration from the parent structs. func processWith( context.Context, *Config) error { := .Target := .Lookuper if == nil { return ErrLookuperNil } := reflect.ValueOf() if .Kind() != reflect.Ptr { return ErrNotPtr } := .Elem() if .Kind() != reflect.Struct { return ErrNotStruct } := .Type() := .DefaultDelimiter if == "" { = "," } := .DefaultNoInit := .DefaultSeparator if == "" { = ":" } := .DefaultOverwrite := .DefaultDecodeUnset := .DefaultRequired := .Mutators for := 0; < .NumField(); ++ { := .Field() := .Field() := .Tag.Get(envTag) if !.CanSet() { if != "" { // There's an "env" tag on a private field, we can't alter it, and it's // likely a mistake. Return an error so the user can handle. return fmt.Errorf("%s: %w", .Name, ErrPrivateField) } // Otherwise continue to the next field. continue } // Parse the key and options. , , := keyAndOpts() if != nil { return fmt.Errorf("%s: %w", .Name, ) } // NoInit is only permitted on pointers. if .NoInit && .Kind() != reflect.Ptr && .Kind() != reflect.Slice && .Kind() != reflect.Map && .Kind() != reflect.UnsafePointer { return fmt.Errorf("%s: %w", .Name, ErrNoInitNotPtr) } // Compute defaults from local tags. := if := .Delimiter; != "" { = } := if := .Separator; != "" { = } := || .NoInit := || .Overwrite := || .DecodeUnset := || .Required := false := func( reflect.Value) { := .Field() if { := reflect.New(.Type().Elem()).Interface() // If a struct (after traversal) equals to the empty value, it means // nothing was changed in any sub-fields. With the noinit opt, we skip // setting the empty value to the original struct pointer (keep it nil). if !reflect.DeepEqual(.Interface(), ) || ! { .Set() } } } // Initialize pointer structs. := false for .Kind() == reflect.Ptr { if .IsNil() { if .Type().Elem().Kind() != reflect.Struct { // This is a nil pointer to something that isn't a struct, like // *string. Move along. break } = true // Use an empty struct of the type so we can traverse. = reflect.New(.Type().Elem()).Elem() } else { = true = .Elem() } } // Special case handle structs. This has to come after the value resolution in // case the struct has a custom decoder. if .Kind() == reflect.Struct { for .CanAddr() { = .Addr() } // Lookup the value, ignoring an error if the key isn't defined. This is // required for nested structs that don't declare their own `env` keys, // but have internal fields with an `env` defined. , , , := lookup(, , .Default, ) if != nil && !errors.Is(, ErrMissingKey) { return fmt.Errorf("%s: %w", .Name, ) } if || || { if , := processAsDecoder(, ); { if != nil { return } () continue } } := if .Prefix != "" { = PrefixLookuper(.Prefix, ) } if := (, &Config{ Target: .Interface(), Lookuper: , DefaultDelimiter: , DefaultSeparator: , DefaultNoInit: , DefaultOverwrite: , DefaultRequired: , Mutators: , }); != nil { return fmt.Errorf("%s: %w", .Name, ) } () continue } // It's invalid to have a prefix on a non-struct field. if .Prefix != "" { return ErrPrefixNotStruct } // Stop processing if there's no env tag (this comes after nested parsing), // in case there's an env tag in an embedded struct. if == "" { continue } // The field already has a non-zero value and overwrite is false, do not // overwrite. if ( || !.IsZero()) && ! { continue } , , , := lookup(, , .Default, ) if != nil { return fmt.Errorf("%s: %w", .Name, ) } // If the field already has a non-zero value and there was no value directly // specified, do not overwrite the existing field. We only want to overwrite // when the envvar was provided directly. if ( || !.IsZero()) && ! { continue } // Apply any mutators. Mutators are applied after the lookup, but before any // type conversions. They always resolve to a string (or error), so we don't // call mutators when the environment variable was not set. if || { := := if , := .(keyedLookuper); { = .Key() } := := false for , := range { , , = .EnvMutate(, , , , ) if != nil { return fmt.Errorf("%s: %w", .Name, ) } if { break } } } // Set value. if := processField(, , , , ); != nil { return fmt.Errorf("%s(%q): %w", .Name, , ) } } return nil } // SplitString splits the given string on the provided rune, unless the rune is // escaped by the escape character. func splitString(, , string) []string { := strings.Split(, ) for := len() - 2; >= 0; -- { if strings.HasSuffix([], ) { [] = [][:len([])-len()] + + [+1] = append([:+1], [+2:]...) } } return } // keyAndOpts parses the given tag value (e.g. env:"foo,required") and // returns the key name and options as a list. func keyAndOpts( string) (string, *options, error) { := splitString(, ",", "\\") , := strings.TrimSpace([0]), [1:] if != "" && !validateEnvName() { return "", nil, fmt.Errorf("%q: %w ", , ErrInvalidEnvvarName) } var options : for , := range { = strings.TrimLeftFunc(, unicode.IsSpace) := strings.ToLower() switch { case == optDecodeUnset: .DecodeUnset = true case == optOverwrite: .Overwrite = true case == optRequired: .Required = true case == optNoInit: .NoInit = true case strings.HasPrefix(, optPrefix): .Prefix = strings.TrimPrefix(, optPrefix) case strings.HasPrefix(, optDelimiter): .Delimiter = strings.TrimPrefix(, optDelimiter) case strings.HasPrefix(, optSeparator): .Separator = strings.TrimPrefix(, optSeparator) case strings.HasPrefix(, optDefault): // If a default value was given, assume everything after is the provided // value, including comma-seprated items. = strings.TrimLeft(strings.Join([:], ","), " ") .Default = strings.TrimPrefix(, optDefault) break default: return "", nil, fmt.Errorf("%q: %w", , ErrUnknownOption) } } return , &, nil } // lookup looks up the given key using the provided Lookuper and options. The // first boolean parameter indicates whether the value was found in the // lookuper. The second boolean parameter indicates whether the default value // was used. func lookup( string, bool, string, Lookuper) (string, bool, bool, error) { if == "" { // The struct has something like `env:",required"`, which is likely a // mistake. We could try to infer the envvar from the field name, but that // feels too magical. return "", false, false, ErrMissingKey } if && != "" { // Having a default value on a required value doesn't make sense. return "", false, false, ErrRequiredAndDefault } // Lookup value. , := .Lookup() if ! { if { if , := .(keyedLookuper); { = .Key() } return "", false, false, fmt.Errorf("%w: %s", ErrMissingRequired, ) } if != "" { // Expand the default value. This allows for a default value that maps to // a different environment variable. = os.Expand(, func( string) string { := if , := .(unwrappableLookuper); { = .Unwrap() } , := .Lookup() if { return } return "" }) return , false, true, nil } } return , , false, nil } // processAsDecoder processes the given value as a decoder or custom // unmarshaller. func processAsDecoder( string, reflect.Value) (bool, error) { // Keep a running error. It's possible that a property might implement // multiple decoders, and we don't know *which* decoder will succeed. If we // get through all of them, we'll return the most recent error. var bool var error // Resolve any pointers. for .CanAddr() { = .Addr() } if .CanInterface() { := .Interface() // If a developer chooses to implement the Decoder interface on a type, // never attempt to use other decoders in case of failure. EnvDecode's // decoding logic is "the right one", and the error returned (if any) // is the most specific we can get. if , := .(Decoder); { = true = .EnvDecode() return , } if , := .(encoding.TextUnmarshaler); { = true if = .UnmarshalText([]byte()); == nil { return , nil } } if , := .(json.Unmarshaler); { = true if = .UnmarshalJSON([]byte()); == nil { return , nil } } if , := .(encoding.BinaryUnmarshaler); { = true if = .UnmarshalBinary([]byte()); == nil { return , nil } } if , := .(gob.GobDecoder); { = true if = .GobDecode([]byte()); == nil { return , nil } } } return , } func processField( string, reflect.Value, , string, bool) error { // If the input value is empty and initialization is skipped, do nothing. if == "" && { return nil } // Handle pointers and uninitialized pointers. for .Type().Kind() == reflect.Ptr { if .IsNil() { .Set(reflect.New(.Type().Elem())) } = .Elem() } := .Type() := .Kind() // Handle existing decoders. if , := processAsDecoder(, ); { return } // We don't check if the value is empty earlier, because the user might want // to define a custom decoder and treat the empty variable as a special case. // However, if we got this far, none of the remaining parsers will succeed, so // bail out now. if == "" { return nil } switch { case reflect.Bool: , := strconv.ParseBool() if != nil { return } .SetBool() case reflect.Float32, reflect.Float64: , := strconv.ParseFloat(, .Bits()) if != nil { return } .SetFloat() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: , := strconv.ParseInt(, 0, .Bits()) if != nil { return } .SetInt() case reflect.Int64: // Special case time.Duration values. if .PkgPath() == "time" && .Name() == "Duration" { , := time.ParseDuration() if != nil { return } .SetInt(int64()) } else { , := strconv.ParseInt(, 0, .Bits()) if != nil { return } .SetInt() } case reflect.String: .SetString() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: , := strconv.ParseUint(, 0, .Bits()) if != nil { return } .SetUint() case reflect.Interface: return fmt.Errorf("cannot decode into interfaces") // Maps case reflect.Map: := strings.Split(, ) := reflect.MakeMapWithSize(, len()) for , := range { := strings.SplitN(, , 2) if len() < 2 { return fmt.Errorf("%s: %w", , ErrInvalidMapItem) } , := strings.TrimSpace([0]), strings.TrimSpace([1]) := reflect.New(.Key()).Elem() if := (, , , , ); != nil { return fmt.Errorf("%s: %w", , ) } := reflect.New(.Elem()).Elem() if := (, , , , ); != nil { return fmt.Errorf("%s: %w", , ) } .SetMapIndex(, ) } .Set() // Slices case reflect.Slice: // Special case: []byte if .Elem().Kind() == reflect.Uint8 { .Set(reflect.ValueOf([]byte())) } else { := strings.Split(, ) := reflect.MakeSlice(, len(), len()) for , := range { = strings.TrimSpace() if := (, .Index(), , , ); != nil { return fmt.Errorf("%s: %w", , ) } } .Set() } } return nil } // validateEnvName validates the given string conforms to being a valid // environment variable. // // Per IEEE Std 1003.1-2001 environment variables consist solely of uppercase // letters, digits, and _, and do not begin with a digit. func validateEnvName( string) bool { if == "" { return false } for , := range { if ( == 0 && !isLetter()) || (!isLetter() && !isNumber() && != '_') { return false } } return true } // isLetter returns true if the given rune is a letter between a-z,A-Z. This is // different than unicode.IsLetter which includes all L character cases. func isLetter( rune) bool { return ( >= 'a' && <= 'z') || ( >= 'A' && <= 'Z') } // isNumber returns true if the given run is a number between 0-9. This is // different than unicode.IsNumber in that it only allows 0-9. func isNumber( rune) bool { return >= '0' && <= '9' }