...
1 package ff
2
3 import (
4 "bufio"
5 "flag"
6 "fmt"
7 "io"
8 "os"
9 "strings"
10 )
11
12
13
14
15 func Parse(fs *flag.FlagSet, args []string, options ...Option) error {
16 var c Context
17 for _, option := range options {
18 option(&c)
19 }
20
21
22 if err := fs.Parse(args); err != nil {
23 return fmt.Errorf("error parsing commandline args: %w", err)
24 }
25
26 provided := map[string]bool{}
27 fs.Visit(func(f *flag.Flag) {
28 provided[f.Name] = true
29 })
30
31
32 if parseEnv := c.envVarPrefix != "" || c.envVarNoPrefix; parseEnv {
33 var visitErr error
34 fs.VisitAll(func(f *flag.Flag) {
35 if visitErr != nil {
36 return
37 }
38
39 if provided[f.Name] {
40 return
41 }
42
43 var key string
44 key = strings.ToUpper(f.Name)
45 key = envVarReplacer.Replace(key)
46 key = maybePrefix(key, c.envVarNoPrefix, c.envVarPrefix)
47
48 value := os.Getenv(key)
49 if value == "" {
50 return
51 }
52
53 for _, v := range maybeSplit(value, c.envVarSplit) {
54 if err := fs.Set(f.Name, v); err != nil {
55 visitErr = fmt.Errorf("error setting flag %q from env var %q: %w", f.Name, key, err)
56 return
57 }
58 }
59 })
60 if visitErr != nil {
61 return fmt.Errorf("error parsing env vars: %w", visitErr)
62 }
63 }
64
65 fs.Visit(func(f *flag.Flag) {
66 provided[f.Name] = true
67 })
68
69 var configFile string
70 if c.configFileVia != nil {
71 configFile = *c.configFileVia
72 }
73
74
75 if configFile == "" && c.configFileFlagName != "" {
76 if f := fs.Lookup(c.configFileFlagName); f != nil {
77 configFile = f.Value.String()
78 }
79 }
80
81 if parseConfig := configFile != "" && c.configFileParser != nil; parseConfig {
82 f, err := os.Open(configFile)
83 switch {
84 case err == nil:
85 defer f.Close()
86 if err := c.configFileParser(f, func(name, value string) error {
87 if provided[name] {
88 return nil
89 }
90
91 defined := fs.Lookup(name) != nil
92 switch {
93 case !defined && c.ignoreUndefined:
94 return nil
95 case !defined && !c.ignoreUndefined:
96 return fmt.Errorf("config file flag %q not defined in flag set", name)
97 }
98
99 if err := fs.Set(name, value); err != nil {
100 return fmt.Errorf("error setting flag %q from config file: %w", name, err)
101 }
102
103 return nil
104 }); err != nil {
105 return err
106 }
107
108 case os.IsNotExist(err) && c.allowMissingConfigFile:
109
110
111 default:
112 return err
113 }
114 }
115
116 fs.Visit(func(f *flag.Flag) {
117 provided[f.Name] = true
118 })
119
120 return nil
121 }
122
123
124 type Context struct {
125 configFileVia *string
126 configFileFlagName string
127 configFileParser ConfigFileParser
128 allowMissingConfigFile bool
129 envVarPrefix string
130 envVarNoPrefix bool
131 envVarSplit string
132 ignoreUndefined bool
133 }
134
135
136 type Option func(*Context)
137
138
139
140
141
142 func WithConfigFile(filename string) Option {
143 return WithConfigFileVia(&filename)
144 }
145
146
147
148
149
150 func WithConfigFileVia(filename *string) Option {
151 return func(c *Context) {
152 c.configFileVia = filename
153 }
154 }
155
156
157
158
159
160
161
162
163 func WithConfigFileFlag(flagname string) Option {
164 return func(c *Context) {
165 c.configFileFlagName = flagname
166 }
167 }
168
169
170
171 func WithConfigFileParser(p ConfigFileParser) Option {
172 return func(c *Context) {
173 c.configFileParser = p
174 }
175 }
176
177
178
179
180 func WithAllowMissingConfigFile(allow bool) Option {
181 return func(c *Context) {
182 c.allowMissingConfigFile = allow
183 }
184 }
185
186
187
188
189
190
191 func WithEnvVarPrefix(prefix string) Option {
192 return func(c *Context) {
193 c.envVarPrefix = prefix
194 }
195 }
196
197
198
199
200
201
202 func WithEnvVarNoPrefix() Option {
203 return func(c *Context) {
204 c.envVarNoPrefix = true
205 }
206 }
207
208
209
210
211 func WithEnvVarSplit(delimiter string) Option {
212 return func(c *Context) {
213 c.envVarSplit = delimiter
214 }
215 }
216
217
218
219
220
221 func WithIgnoreUndefined(ignore bool) Option {
222 return func(c *Context) {
223 c.ignoreUndefined = ignore
224 }
225 }
226
227
228
229 type ConfigFileParser func(r io.Reader, set func(name, value string) error) error
230
231
232
233
234
235
236 func PlainParser(r io.Reader, set func(name, value string) error) error {
237 s := bufio.NewScanner(r)
238 for s.Scan() {
239 line := strings.TrimSpace(s.Text())
240 if line == "" {
241 continue
242 }
243
244 if line[0] == '#' {
245 continue
246 }
247
248 var (
249 name string
250 value string
251 index = strings.IndexRune(line, ' ')
252 )
253 if index < 0 {
254 name, value = line, "true"
255 } else {
256 name, value = line[:index], strings.TrimSpace(line[index:])
257 }
258
259 if i := strings.Index(value, " #"); i >= 0 {
260 value = strings.TrimSpace(value[:i])
261 }
262
263 if err := set(name, value); err != nil {
264 return err
265 }
266 }
267 return nil
268 }
269
270 var envVarReplacer = strings.NewReplacer(
271 "-", "_",
272 ".", "_",
273 "/", "_",
274 )
275
276 func maybePrefix(key string, noPrefix bool, prefix string) string {
277 if noPrefix {
278 return key
279 }
280 return strings.ToUpper(prefix) + "_" + key
281 }
282
283 func maybeSplit(value, split string) []string {
284 if split == "" {
285 return []string{value}
286 }
287 return strings.Split(value, split)
288 }
289
View as plain text