env.go raw
1 // Package env implements loading environment variables into a config struct.
2 package env
3
4 import (
5 "fmt"
6 "os"
7 "reflect"
8 "strings"
9 )
10
11 // Options are the options for the [Load] and [Usage] functions.
12 type Options struct {
13 Source Source // The source of environment variables. The default is [OS].
14 SliceSep string // The separator used to parse slice values. The default is space.
15 NameSep string // The separator used to concatenate environment variable names from nested struct tags. The default is an empty string.
16 }
17
18 // NotSetError is returned when required environment variables are not set.
19 type NotSetError struct {
20 Names []string
21 }
22
23 // Error implements the error interface.
24 func (e *NotSetError) Error() string {
25 if len(e.Names) == 1 {
26 return fmt.Sprintf("env: %s is required but not set", e.Names[0])
27 }
28 return fmt.Sprintf("env: %s are required but not set", strings.Join(e.Names, " "))
29 }
30
31 // Load loads environment variables into the given struct.
32 // cfg must be a non-nil struct pointer, otherwise Load panics.
33 // If opts is nil, the default [Options] are used.
34 //
35 // The struct fields must have the `env:"VAR"` struct tag,
36 // where VAR is the name of the corresponding environment variable.
37 // Unexported fields are ignored.
38 //
39 // The following types are supported:
40 // - int (any kind)
41 // - float (any kind)
42 // - bool
43 // - string
44 // - [time.Duration]
45 // - [encoding.TextUnmarshaler]
46 // - slices of any type above
47 // - nested structs of any depth
48 //
49 // See the [strconv].Parse* functions for the parsing rules.
50 // User-defined types can be used by implementing the [encoding.TextUnmarshaler] interface.
51 //
52 // Nested struct of any depth level are supported,
53 // allowing grouping of related environment variables.
54 // If a nested struct has the optional `env:"PREFIX"` tag,
55 // the environment variables declared by its fields are prefixed with PREFIX.
56 //
57 // Default values can be specified using the `default:"VALUE"` struct tag.
58 //
59 // The name of an environment variable can be followed by comma-separated options:
60 // - required: marks the environment variable as required
61 // - expand: expands the value of the environment variable using [os.Expand]
62 func Load(cfg any, opts *Options) error {
63 pv := reflect.ValueOf(cfg)
64 if !structPtr(pv) {
65 panic("env: cfg must be a non-nil struct pointer")
66 }
67
68 opts = setDefaultOptions(opts)
69
70 v := pv.Elem()
71 vars := parseVars(v, opts)
72 cache[v.Type()] = vars
73
74 var notset []string
75 for _, v := range vars {
76 value, ok := lookupEnv(opts.Source, v.Name, v.Expand)
77 if !ok {
78 if v.Required {
79 notset = append(notset, v.Name)
80 continue
81 }
82 if !v.hasDefaultTag {
83 continue // nothing to set.
84 }
85 value = v.Default
86 }
87
88 var err error
89 if kindOf(v.structField, reflect.Slice) && !implements(v.structField, unmarshalerIface) {
90 err = setSlice(v.structField, strings.Split(value, opts.SliceSep))
91 } else {
92 err = setValue(v.structField, value)
93 }
94 if err != nil {
95 return err
96 }
97 }
98
99 if len(notset) > 0 {
100 return &NotSetError{Names: notset}
101 }
102
103 return nil
104 }
105
106 func setDefaultOptions(opts *Options) *Options {
107 if opts == nil {
108 opts = new(Options)
109 }
110 if opts.Source == nil {
111 opts.Source = OS
112 }
113 if opts.SliceSep == "" {
114 opts.SliceSep = " "
115 }
116 return opts
117 }
118
119 func parseVars(v reflect.Value, opts *Options) []Var {
120 var vars []Var
121
122 for i := 0; i < v.NumField(); i++ {
123 field := v.Field(i)
124 if !field.CanSet() {
125 continue
126 }
127
128 tags := v.Type().Field(i).Tag
129
130 if kindOf(field, reflect.Struct) && !implements(field, unmarshalerIface) {
131 var prefix string
132 if value, ok := tags.Lookup("env"); ok {
133 prefix = value + opts.NameSep
134 }
135 for _, v := range parseVars(field, opts) {
136 v.Name = prefix + v.Name
137 vars = append(vars, v)
138 }
139 continue
140 }
141
142 value, ok := tags.Lookup("env")
143 if !ok {
144 continue
145 }
146
147 parts := strings.Split(value, ",")
148 name, options := parts[0], parts[1:]
149 if name == "" {
150 panic("env: empty tag name is not allowed")
151 }
152
153 var required, expand bool
154 for _, option := range options {
155 switch option {
156 case "required":
157 required = true
158 case "expand":
159 expand = true
160 default:
161 panic(fmt.Sprintf("env: invalid tag option `%s`", option))
162 }
163 }
164
165 defValue, defSet := tags.Lookup("default")
166 switch {
167 case defSet && required:
168 panic("env: `required` and `default` can't be used simultaneously")
169 case !defSet && !required:
170 defValue = fmt.Sprintf("%v", field.Interface())
171 }
172
173 vars = append(vars, Var{
174 Name: name,
175 Type: field.Type(),
176 Usage: tags.Get("usage"),
177 Default: defValue,
178 Required: required,
179 Expand: expand,
180 structField: field,
181 hasDefaultTag: defSet,
182 })
183 }
184
185 return vars
186 }
187
188 func lookupEnv(src Source, key string, expand bool) (string, bool) {
189 value, ok := src.LookupEnv(key)
190 if !ok {
191 return "", false
192 }
193 if !expand {
194 return value, true
195 }
196 mapping := func(key string) string {
197 v, _ := src.LookupEnv(key)
198 return v
199 }
200 return os.Expand(value, mapping), true
201 }
202