This repository was archived by the owner on Oct 16, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconfig.go
More file actions
349 lines (328 loc) · 12.1 KB
/
Copy pathconfig.go
File metadata and controls
349 lines (328 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
/*
Package flagtag provides support for creating command line flags by tagging appropriate struct fields with the 'flag' tag.
*/
package flagtag
import (
"errors"
"flag"
"os"
"reflect"
"strconv"
"strings"
"time"
"unsafe"
)
// MustConfigureAndParse is like ConfigureAndParse, the only difference is that
// it will panic in case of an error.
func MustConfigureAndParse(config interface{}) {
if err := ConfigureAndParse(config); err != nil {
panic(err)
}
}
// MustConfigureFlagsetAndParse is like MustConfigureAndParse with the addition
// that it is possible to provide a custom flagset.
func MustConfigureFlagsetAndParse(config interface{}, flagset *flag.FlagSet) {
if err := ConfigureFlagsetAndParse(config, flagset); err != nil {
panic(err)
}
}
// MustConfigureAndParseArgs is like MustConfigureAndParse with the addition
// that it is possible to provide an arguments slice to be parsed, instead of
// the default command line arguments slice.
func MustConfigureAndParseArgs(config interface{}, args []string) {
if err := ConfigureAndParseArgs(config, args); err != nil {
panic(err)
}
}
// MustConfigureFlagsetAndParseArgs is like MustConfigureAndParse with the
// addition that it is possible to provide both a custom flagset and the
// argument slice to be parsed.
func MustConfigureFlagsetAndParseArgs(config interface{}, flagset *flag.FlagSet, args []string) {
if err := ConfigureFlagsetAndParseArgs(config, flagset, args); err != nil {
panic(err)
}
}
// MustConfigure is like Configure, the only difference is that it will panic
// in case of an error.
func MustConfigure(config interface{}) {
if err := Configure(config); err != nil {
panic(err)
}
}
// MustConfigureFlagset is like Configure, the only difference being that it is
// possible to provide a custom flagset.
func MustConfigureFlagset(config interface{}, flagset *flag.FlagSet) {
if err := ConfigureFlagset(config, flagset); err != nil {
panic(err)
}
}
// ConfigureAndParse will first attempt to configure the flags according to the
// provided config type. If any error occurs, this error will be returned and
// the command line arguments will not be parsed. If no error occurs, the
// command line arguments will be parsed and the config type will contain the
// result.
// Using this function may remove the need to even import the flag package at
// all.
func ConfigureAndParse(config interface{}) error {
if err := Configure(config); err != nil {
return err
}
flag.Parse()
return nil
}
// ConfigureFlagsetAndParse is like ConfigureAndParse with the addition that it
// is possible to provide the flagset for the configuration.
func ConfigureFlagsetAndParse(config interface{}, flagset *flag.FlagSet) error {
return ConfigureFlagsetAndParseArgs(config, flagset, os.Args[1:])
}
// ConfigureAndParseArgs is like ConfigureAndParse with the addition that it is
// possible to provide the arguments slice that should be parsed.
func ConfigureAndParseArgs(config interface{}, args []string) error {
return ConfigureFlagsetAndParseArgs(config, flag.CommandLine, args)
}
// ConfigureFlagsetAndParseArgs is like ConfigureAndParse with the addition
// that it is possible to provide both the flagset for configuration and the
// arguments slice that should be parsed.
func ConfigureFlagsetAndParseArgs(config interface{}, flagset *flag.FlagSet, args []string) error {
if err := ConfigureFlagset(config, flagset); err != nil {
return err
}
return flagset.Parse(args)
}
// Configure will configure the flag parameters according to the tags of the
// provided data type. It is allowed to call this method multiple times with
// different data types. (As long as flag's Parse() method has not been called
// yet.)
// Fields without a 'flag' tag or with an empty 'flag' tag will be ignored.
//
// The 'flag' tag consists of 3 parts, similar to the *Var-functions of the
// flag package. Parts are separated by a comma. The parts are:
// - 1st: flag name
// - 2nd: default value
// - 3rd: usage description
//
// Example:
// `flag:"verbose,false,Enable verbose output."`.
//
// This will create a flag 'verbose', which defaults to 'false' and shows usage
// information "Enable verbose output.".
//
// If an error occurs, this error will be returned and the configuration of
// other struct fields will be aborted.
func Configure(config interface{}) error {
return ConfigureFlagset(config, flag.CommandLine)
}
// ConfigureFlagset is like Configure but with the added ability to provide a
// flag set.
func ConfigureFlagset(config interface{}, flagset *flag.FlagSet) error {
val, err := getStructValue(config)
if err != nil {
return err
}
return configure(val, flagset)
}
// configure (recursively) configures flags as they are discovered in the provided type and value.
// In case of an error, the error is returned. Possible errors are:
// - Invalid default values, error of type ErrInvalidDefault.
// - nil pointer provided.
// - nil interface provided.
// - interface to nil value provided.
// - Tagged variable uses unsupported data type.
func configure(structValue reflect.Value, flagset *flag.FlagSet) error {
if flagset == nil {
return errors.New("flagset cannot be nil")
}
var structType = structValue.Type()
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
fieldType := field.Type
fieldValue := structValue.Field(i)
t := field.Tag.Get("flag")
if t == "" {
// if field is not tagged then we do not need to flag the type itself
if fieldType.Kind() == reflect.Struct {
// kind is a struct => recurse into inner struct
if err := configure(fieldValue, flagset); err != nil {
return err
}
}
} else {
// field is tagged, continue investigating what kind of flag to create
tag := parseTag(t, field.Tag.Get("flagopt"))
if tag.Name == "" {
// tag is invalid, since there is no name
return errors.New("field '" + field.Name + "': invalid flag name: empty string")
}
switch fieldType.Kind() {
case reflect.Ptr:
// unwrap pointer
if fieldValue.IsNil() {
return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "'): cannot use nil pointer")
}
fieldType = fieldType.Elem()
fieldValue = fieldValue.Elem()
case reflect.Interface:
// check if interface is valid
if fieldValue.IsNil() {
return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "'): cannot use nil interface")
}
var value = reflect.ValueOf(fieldValue.Interface())
switch value.Type().Kind() {
case reflect.Ptr, reflect.Interface:
if value.IsNil() {
return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "'): cannot use nil interface value")
}
}
}
if !fieldValue.CanSet() {
return errors.New("field '" + field.Name + "' (tag '" + tag.Name + "') is unexported or unaddressable: cannot use this field")
}
if !tag.Options.SkipFlagValue && registerFlagByValueInterface(fieldValue, &tag, flagset) {
// no error during registration => Var-flag registered => continue with next field
continue
}
if err := registerFlagByPrimitive(field.Name, fieldValue, &tag, flagset); err != nil {
return err
}
}
}
return nil
}
// registerFlagByValueInterface checks if the provided type can be treated as flag.Value.
// If so, a flag.Value flag is set and true is returned. If no flag is set, false is returned.
func registerFlagByValueInterface(fieldValue reflect.Value, tag *flagTag, flagset *flag.FlagSet) bool {
var value flag.Value
switch fieldValue.Type().Kind() {
case reflect.Interface:
var ok bool
value, ok = fieldValue.Interface().(flag.Value)
if !ok {
return false
}
default:
var ok bool
value, ok = fieldValue.Addr().Interface().(flag.Value)
if !ok {
return false
}
}
flagset.Var(value, tag.Name, tag.Description)
if tag.DefaultValue != "" {
// a default value is provided, first call value.Set() with the provided default value
value.Set(tag.DefaultValue)
}
return true
}
// registerFlagByPrimitive registers a single field as one of the primitive flag types. Types are matched by
// kind, so types derived from one of the basic types are still eligible for a flag.
//
// If it is not possible to register a flag because of an unknown data type, an error will be returned.
// If the specified default value is invalid, an error of type ErrInvalidDefault will be returned.
func registerFlagByPrimitive(fieldName string, fieldValue reflect.Value, tag *flagTag, flagset *flag.FlagSet) error {
var fieldType = fieldValue.Type()
// Check time.Duration first, since it will also match one of the basic kinds.
if durationVar, ok := fieldValue.Addr().Interface().(*time.Duration); ok {
// field is a time.Duration
defaultVal, err := time.ParseDuration(tag.DefaultValue)
if err != nil {
return &ErrInvalidDefault{fieldName, tag.Name, err}
}
flagset.DurationVar(durationVar, tag.Name, defaultVal, tag.Description)
return nil
}
// Check basic kinds.
// TODO convert to detected kind without using unsafe
var fieldPtr = unsafe.Pointer(fieldValue.UnsafeAddr())
switch fieldType.Kind() {
case reflect.String:
flagset.StringVar((*string)(fieldPtr), tag.Name, tag.DefaultValue, tag.Description)
case reflect.Bool:
defaultVal, err := strconv.ParseBool(tag.DefaultValue)
if err != nil {
return &ErrInvalidDefault{fieldName, tag.Name, err}
}
flagset.BoolVar((*bool)(fieldPtr), tag.Name, defaultVal, tag.Description)
case reflect.Float64:
defaultVal, err := strconv.ParseFloat(tag.DefaultValue, 64)
if err != nil {
return &ErrInvalidDefault{fieldName, tag.Name, err}
}
flagset.Float64Var((*float64)(fieldPtr), tag.Name, defaultVal, tag.Description)
case reflect.Int:
defaultVal, err := strconv.ParseInt(tag.DefaultValue, 0, fieldType.Bits())
if err != nil {
return &ErrInvalidDefault{fieldName, tag.Name, err}
}
flagset.IntVar((*int)(fieldPtr), tag.Name, int(defaultVal), tag.Description)
case reflect.Int64:
defaultVal, err := strconv.ParseInt(tag.DefaultValue, 0, 64)
if err != nil {
return &ErrInvalidDefault{fieldName, tag.Name, err}
}
flagset.Int64Var((*int64)(fieldPtr), tag.Name, defaultVal, tag.Description)
case reflect.Uint:
defaultVal, err := strconv.ParseUint(tag.DefaultValue, 0, fieldType.Bits())
if err != nil {
return &ErrInvalidDefault{fieldName, tag.Name, err}
}
flagset.UintVar((*uint)(fieldPtr), tag.Name, uint(defaultVal), tag.Description)
case reflect.Uint64:
defaultVal, err := strconv.ParseUint(tag.DefaultValue, 0, 64)
if err != nil {
return &ErrInvalidDefault{fieldName, tag.Name, err}
}
flagset.Uint64Var((*uint64)(fieldPtr), tag.Name, defaultVal, tag.Description)
default:
return errors.New("unsupported data type (kind '" + strconv.FormatUint(uint64(fieldType.Kind()), 10) + "') for field '" + fieldName + "' (tag '" + tag.Name + "')")
}
return nil
}
// getStructValue checks that the provided config instance is actually a struct not a nil value.
func getStructValue(config interface{}) (reflect.Value, error) {
var zero reflect.Value
if config == nil {
return zero, errors.New("config cannot be nil")
}
ptr := reflect.ValueOf(config)
if ptr.IsNil() {
return zero, errors.New("config cannot point to nil")
}
val := reflect.Indirect(ptr)
if val.Kind() != reflect.Struct {
return zero, errors.New("config instance is not a struct")
}
return val, nil
}
// parseTag parses a string of text and separates the various sections of the 'flag'-tag.
func parseTag(value string, optvalue string) flagTag {
parts := strings.SplitN(value, ",", 3)
for len(parts) < 3 {
parts = append(parts, "")
}
var flag = flagTag{Name: parts[0], DefaultValue: parts[1], Description: parts[2]}
if optvalue != "" {
if strings.Contains(optvalue, "skipFlagValue") {
flag.Options.SkipFlagValue = true
}
}
return flag
}
// flagTag contains the parsed tag values.
type flagTag struct {
Name string
DefaultValue string
Description string
Options struct {
SkipFlagValue bool
}
}
// ErrInvalidDefault is an error type for the case of invalid defaults.
type ErrInvalidDefault struct {
field string
tag string
err error
}
// Error returns the error explaining the bad default value.
func (e *ErrInvalidDefault) Error() string {
return "invalid default value for field '" + e.field + "' (tag '" + e.tag + "'): " + e.err.Error()
}