blob: 181caabb2df8359981bf1dedbbb15fe444c92d38 [file] [log] [blame]
Naveen Sampath04696f72022-06-13 15:19:14 +05301// Copyright 2012 Jesse van den Kieboom. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package flags
6
7import (
8 "errors"
9 "reflect"
10 "strings"
11 "unicode/utf8"
12)
13
14// ErrNotPointerToStruct indicates that a provided data container is not
15// a pointer to a struct. Only pointers to structs are valid data containers
16// for options.
17var ErrNotPointerToStruct = errors.New("provided data is not a pointer to struct")
18
19// Group represents an option group. Option groups can be used to logically
20// group options together under a description. Groups are only used to provide
21// more structure to options both for the user (as displayed in the help message)
22// and for you, since groups can be nested.
23type Group struct {
24 // A short description of the group. The
25 // short description is primarily used in the built-in generated help
26 // message
27 ShortDescription string
28
29 // A long description of the group. The long
30 // description is primarily used to present information on commands
31 // (Command embeds Group) in the built-in generated help and man pages.
32 LongDescription string
33
34 // The namespace of the group
35 Namespace string
36
37 // The environment namespace of the group
38 EnvNamespace string
39
40 // If true, the group is not displayed in the help or man page
41 Hidden bool
42
43 // The parent of the group or nil if it has no parent
44 parent interface{}
45
46 // All the options in the group
47 options []*Option
48
49 // All the subgroups
50 groups []*Group
51
52 // Whether the group represents the built-in help group
53 isBuiltinHelp bool
54
55 data interface{}
56}
57
58type scanHandler func(reflect.Value, *reflect.StructField) (bool, error)
59
60// AddGroup adds a new group to the command with the given name and data. The
61// data needs to be a pointer to a struct from which the fields indicate which
62// options are in the group.
63func (g *Group) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) {
64 group := newGroup(shortDescription, longDescription, data)
65
66 group.parent = g
67
68 if err := group.scan(); err != nil {
69 return nil, err
70 }
71
72 g.groups = append(g.groups, group)
73 return group, nil
74}
75
76// AddOption adds a new option to this group.
77func (g *Group) AddOption(option *Option, data interface{}) {
78 option.value = reflect.ValueOf(data)
79 option.group = g
80 g.options = append(g.options, option)
81}
82
83// Groups returns the list of groups embedded in this group.
84func (g *Group) Groups() []*Group {
85 return g.groups
86}
87
88// Options returns the list of options in this group.
89func (g *Group) Options() []*Option {
90 return g.options
91}
92
93// Find locates the subgroup with the given short description and returns it.
94// If no such group can be found Find will return nil. Note that the description
95// is matched case insensitively.
96func (g *Group) Find(shortDescription string) *Group {
97 lshortDescription := strings.ToLower(shortDescription)
98
99 var ret *Group
100
101 g.eachGroup(func(gg *Group) {
102 if gg != g && strings.ToLower(gg.ShortDescription) == lshortDescription {
103 ret = gg
104 }
105 })
106
107 return ret
108}
109
110func (g *Group) findOption(matcher func(*Option) bool) (option *Option) {
111 g.eachGroup(func(g *Group) {
112 for _, opt := range g.options {
113 if option == nil && matcher(opt) {
114 option = opt
115 }
116 }
117 })
118
119 return option
120}
121
122// FindOptionByLongName finds an option that is part of the group, or any of its
123// subgroups, by matching its long name (including the option namespace).
124func (g *Group) FindOptionByLongName(longName string) *Option {
125 return g.findOption(func(option *Option) bool {
126 return option.LongNameWithNamespace() == longName
127 })
128}
129
130// FindOptionByShortName finds an option that is part of the group, or any of
131// its subgroups, by matching its short name.
132func (g *Group) FindOptionByShortName(shortName rune) *Option {
133 return g.findOption(func(option *Option) bool {
134 return option.ShortName == shortName
135 })
136}
137
138func newGroup(shortDescription string, longDescription string, data interface{}) *Group {
139 return &Group{
140 ShortDescription: shortDescription,
141 LongDescription: longDescription,
142
143 data: data,
144 }
145}
146
147func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option {
148 prio := 0
149 var retopt *Option
150
151 g.eachGroup(func(g *Group) {
152 for _, opt := range g.options {
153 if namematch != nil && namematch(opt, name) && prio < 4 {
154 retopt = opt
155 prio = 4
156 }
157
158 if name == opt.field.Name && prio < 3 {
159 retopt = opt
160 prio = 3
161 }
162
163 if name == opt.LongNameWithNamespace() && prio < 2 {
164 retopt = opt
165 prio = 2
166 }
167
168 if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 {
169 retopt = opt
170 prio = 1
171 }
172 }
173 })
174
175 return retopt
176}
177
178func (g *Group) showInHelp() bool {
179 if g.Hidden {
180 return false
181 }
182 for _, opt := range g.options {
183 if opt.showInHelp() {
184 return true
185 }
186 }
187 return false
188}
189
190func (g *Group) eachGroup(f func(*Group)) {
191 f(g)
192
193 for _, gg := range g.groups {
194 gg.eachGroup(f)
195 }
196}
197
198func isStringFalsy(s string) bool {
199 return s == "" || s == "false" || s == "no" || s == "0"
200}
201
202func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error {
203 stype := realval.Type()
204
205 if sfield != nil {
206 if ok, err := handler(realval, sfield); err != nil {
207 return err
208 } else if ok {
209 return nil
210 }
211 }
212
213 for i := 0; i < stype.NumField(); i++ {
214 field := stype.Field(i)
215
216 // PkgName is set only for non-exported fields, which we ignore
217 if field.PkgPath != "" && !field.Anonymous {
218 continue
219 }
220
221 mtag := newMultiTag(string(field.Tag))
222
223 if err := mtag.Parse(); err != nil {
224 return err
225 }
226
227 // Skip fields with the no-flag tag
228 if mtag.Get("no-flag") != "" {
229 continue
230 }
231
232 // Dive deep into structs or pointers to structs
233 kind := field.Type.Kind()
234 fld := realval.Field(i)
235
236 if kind == reflect.Struct {
237 if err := g.scanStruct(fld, &field, handler); err != nil {
238 return err
239 }
240 } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct {
241 flagCountBefore := len(g.options) + len(g.groups)
242
243 if fld.IsNil() {
244 fld = reflect.New(fld.Type().Elem())
245 }
246
247 if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil {
248 return err
249 }
250
251 if len(g.options)+len(g.groups) != flagCountBefore {
252 realval.Field(i).Set(fld)
253 }
254 }
255
256 longname := mtag.Get("long")
257 shortname := mtag.Get("short")
258
259 // Need at least either a short or long name
260 if longname == "" && shortname == "" && mtag.Get("ini-name") == "" {
261 continue
262 }
263
264 short := rune(0)
265 rc := utf8.RuneCountInString(shortname)
266
267 if rc > 1 {
268 return newErrorf(ErrShortNameTooLong,
269 "short names can only be 1 character long, not `%s'",
270 shortname)
271
272 } else if rc == 1 {
273 short, _ = utf8.DecodeRuneInString(shortname)
274 }
275
276 description := mtag.Get("description")
277 def := mtag.GetMany("default")
278
279 optionalValue := mtag.GetMany("optional-value")
280 valueName := mtag.Get("value-name")
281 defaultMask := mtag.Get("default-mask")
282
283 optional := !isStringFalsy(mtag.Get("optional"))
284 required := !isStringFalsy(mtag.Get("required"))
285 choices := mtag.GetMany("choice")
286 hidden := !isStringFalsy(mtag.Get("hidden"))
287
288 option := &Option{
289 Description: description,
290 ShortName: short,
291 LongName: longname,
292 Default: def,
293 EnvDefaultKey: mtag.Get("env"),
294 EnvDefaultDelim: mtag.Get("env-delim"),
295 OptionalArgument: optional,
296 OptionalValue: optionalValue,
297 Required: required,
298 ValueName: valueName,
299 DefaultMask: defaultMask,
300 Choices: choices,
301 Hidden: hidden,
302
303 group: g,
304
305 field: field,
306 value: realval.Field(i),
307 tag: mtag,
308 }
309
310 if option.isBool() && option.Default != nil {
311 return newErrorf(ErrInvalidTag,
312 "boolean flag `%s' may not have default values, they always default to `false' and can only be turned on",
313 option.shortAndLongName())
314 }
315
316 g.options = append(g.options, option)
317 }
318
319 return nil
320}
321
322func (g *Group) checkForDuplicateFlags() *Error {
323 shortNames := make(map[rune]*Option)
324 longNames := make(map[string]*Option)
325
326 var duplicateError *Error
327
328 g.eachGroup(func(g *Group) {
329 for _, option := range g.options {
330 if option.LongName != "" {
331 longName := option.LongNameWithNamespace()
332
333 if otherOption, ok := longNames[longName]; ok {
334 duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption)
335 return
336 }
337 longNames[longName] = option
338 }
339 if option.ShortName != 0 {
340 if otherOption, ok := shortNames[option.ShortName]; ok {
341 duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption)
342 return
343 }
344 shortNames[option.ShortName] = option
345 }
346 }
347 })
348
349 return duplicateError
350}
351
352func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) {
353 mtag := newMultiTag(string(sfield.Tag))
354
355 if err := mtag.Parse(); err != nil {
356 return true, err
357 }
358
359 subgroup := mtag.Get("group")
360
361 if len(subgroup) != 0 {
362 var ptrval reflect.Value
363
364 if realval.Kind() == reflect.Ptr {
365 ptrval = realval
366
367 if ptrval.IsNil() {
368 ptrval.Set(reflect.New(ptrval.Type()))
369 }
370 } else {
371 ptrval = realval.Addr()
372 }
373
374 description := mtag.Get("description")
375
376 group, err := g.AddGroup(subgroup, description, ptrval.Interface())
377
378 if err != nil {
379 return true, err
380 }
381
382 group.Namespace = mtag.Get("namespace")
383 group.EnvNamespace = mtag.Get("env-namespace")
384 group.Hidden = mtag.Get("hidden") != ""
385
386 return true, nil
387 }
388
389 return false, nil
390}
391
392func (g *Group) scanType(handler scanHandler) error {
393 // Get all the public fields in the data struct
394 ptrval := reflect.ValueOf(g.data)
395
396 if ptrval.Type().Kind() != reflect.Ptr {
397 panic(ErrNotPointerToStruct)
398 }
399
400 stype := ptrval.Type().Elem()
401
402 if stype.Kind() != reflect.Struct {
403 panic(ErrNotPointerToStruct)
404 }
405
406 realval := reflect.Indirect(ptrval)
407
408 if err := g.scanStruct(realval, nil, handler); err != nil {
409 return err
410 }
411
412 if err := g.checkForDuplicateFlags(); err != nil {
413 return err
414 }
415
416 return nil
417}
418
419func (g *Group) scan() error {
420 return g.scanType(g.scanSubGroupHandler)
421}
422
423func (g *Group) groupByName(name string) *Group {
424 if len(name) == 0 {
425 return g
426 }
427
428 return g.Find(name)
429}