1// 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// Groups returns the list of groups embedded in this group.
77func (g *Group) Groups() []*Group {
78	return g.groups
79}
80
81// Options returns the list of options in this group.
82func (g *Group) Options() []*Option {
83	return g.options
84}
85
86// Find locates the subgroup with the given short description and returns it.
87// If no such group can be found Find will return nil. Note that the description
88// is matched case insensitively.
89func (g *Group) Find(shortDescription string) *Group {
90	lshortDescription := strings.ToLower(shortDescription)
91
92	var ret *Group
93
94	g.eachGroup(func(gg *Group) {
95		if gg != g && strings.ToLower(gg.ShortDescription) == lshortDescription {
96			ret = gg
97		}
98	})
99
100	return ret
101}
102
103func (g *Group) findOption(matcher func(*Option) bool) (option *Option) {
104	g.eachGroup(func(g *Group) {
105		for _, opt := range g.options {
106			if option == nil && matcher(opt) {
107				option = opt
108			}
109		}
110	})
111
112	return option
113}
114
115// FindOptionByLongName finds an option that is part of the group, or any of its
116// subgroups, by matching its long name (including the option namespace).
117func (g *Group) FindOptionByLongName(longName string) *Option {
118	return g.findOption(func(option *Option) bool {
119		return option.LongNameWithNamespace() == longName
120	})
121}
122
123// FindOptionByShortName finds an option that is part of the group, or any of
124// its subgroups, by matching its short name.
125func (g *Group) FindOptionByShortName(shortName rune) *Option {
126	return g.findOption(func(option *Option) bool {
127		return option.ShortName == shortName
128	})
129}
130
131func newGroup(shortDescription string, longDescription string, data interface{}) *Group {
132	return &Group{
133		ShortDescription: shortDescription,
134		LongDescription:  longDescription,
135
136		data: data,
137	}
138}
139
140func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option {
141	prio := 0
142	var retopt *Option
143
144	g.eachGroup(func(g *Group) {
145		for _, opt := range g.options {
146			if namematch != nil && namematch(opt, name) && prio < 4 {
147				retopt = opt
148				prio = 4
149			}
150
151			if name == opt.field.Name && prio < 3 {
152				retopt = opt
153				prio = 3
154			}
155
156			if name == opt.LongNameWithNamespace() && prio < 2 {
157				retopt = opt
158				prio = 2
159			}
160
161			if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 {
162				retopt = opt
163				prio = 1
164			}
165		}
166	})
167
168	return retopt
169}
170
171func (g *Group) showInHelp() bool {
172	if g.Hidden {
173		return false
174	}
175	for _, opt := range g.options {
176		if opt.showInHelp() {
177			return true
178		}
179	}
180	return false
181}
182
183func (g *Group) eachGroup(f func(*Group)) {
184	f(g)
185
186	for _, gg := range g.groups {
187		gg.eachGroup(f)
188	}
189}
190
191func isStringFalsy(s string) bool {
192	return s == "" || s == "false" || s == "no" || s == "0"
193}
194
195func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error {
196	stype := realval.Type()
197
198	if sfield != nil {
199		if ok, err := handler(realval, sfield); err != nil {
200			return err
201		} else if ok {
202			return nil
203		}
204	}
205
206	for i := 0; i < stype.NumField(); i++ {
207		field := stype.Field(i)
208
209		// PkgName is set only for non-exported fields, which we ignore
210		if field.PkgPath != "" && !field.Anonymous {
211			continue
212		}
213
214		mtag := newMultiTag(string(field.Tag))
215
216		if err := mtag.Parse(); err != nil {
217			return err
218		}
219
220		// Skip fields with the no-flag tag
221		if mtag.Get("no-flag") != "" {
222			continue
223		}
224
225		// Dive deep into structs or pointers to structs
226		kind := field.Type.Kind()
227		fld := realval.Field(i)
228
229		if kind == reflect.Struct {
230			if err := g.scanStruct(fld, &field, handler); err != nil {
231				return err
232			}
233		} else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct {
234			flagCountBefore := len(g.options) + len(g.groups)
235
236			if fld.IsNil() {
237				fld = reflect.New(fld.Type().Elem())
238			}
239
240			if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil {
241				return err
242			}
243
244			if len(g.options)+len(g.groups) != flagCountBefore {
245				realval.Field(i).Set(fld)
246			}
247		}
248
249		longname := mtag.Get("long")
250		shortname := mtag.Get("short")
251
252		// Need at least either a short or long name
253		if longname == "" && shortname == "" && mtag.Get("ini-name") == "" {
254			continue
255		}
256
257		short := rune(0)
258		rc := utf8.RuneCountInString(shortname)
259
260		if rc > 1 {
261			return newErrorf(ErrShortNameTooLong,
262				"short names can only be 1 character long, not `%s'",
263				shortname)
264
265		} else if rc == 1 {
266			short, _ = utf8.DecodeRuneInString(shortname)
267		}
268
269		description := mtag.Get("description")
270		def := mtag.GetMany("default")
271
272		optionalValue := mtag.GetMany("optional-value")
273		valueName := mtag.Get("value-name")
274		defaultMask := mtag.Get("default-mask")
275
276		optional := !isStringFalsy(mtag.Get("optional"))
277		required := !isStringFalsy(mtag.Get("required"))
278		choices := mtag.GetMany("choice")
279		hidden := !isStringFalsy(mtag.Get("hidden"))
280
281		option := &Option{
282			Description:      description,
283			ShortName:        short,
284			LongName:         longname,
285			Default:          def,
286			EnvDefaultKey:    mtag.Get("env"),
287			EnvDefaultDelim:  mtag.Get("env-delim"),
288			OptionalArgument: optional,
289			OptionalValue:    optionalValue,
290			Required:         required,
291			ValueName:        valueName,
292			DefaultMask:      defaultMask,
293			Choices:          choices,
294			Hidden:           hidden,
295
296			group: g,
297
298			field: field,
299			value: realval.Field(i),
300			tag:   mtag,
301		}
302
303		if option.isBool() && option.Default != nil {
304			return newErrorf(ErrInvalidTag,
305				"boolean flag `%s' may not have default values, they always default to `false' and can only be turned on",
306				option.shortAndLongName())
307		}
308
309		g.options = append(g.options, option)
310	}
311
312	return nil
313}
314
315func (g *Group) checkForDuplicateFlags() *Error {
316	shortNames := make(map[rune]*Option)
317	longNames := make(map[string]*Option)
318
319	var duplicateError *Error
320
321	g.eachGroup(func(g *Group) {
322		for _, option := range g.options {
323			if option.LongName != "" {
324				longName := option.LongNameWithNamespace()
325
326				if otherOption, ok := longNames[longName]; ok {
327					duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption)
328					return
329				}
330				longNames[longName] = option
331			}
332			if option.ShortName != 0 {
333				if otherOption, ok := shortNames[option.ShortName]; ok {
334					duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption)
335					return
336				}
337				shortNames[option.ShortName] = option
338			}
339		}
340	})
341
342	return duplicateError
343}
344
345func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) {
346	mtag := newMultiTag(string(sfield.Tag))
347
348	if err := mtag.Parse(); err != nil {
349		return true, err
350	}
351
352	subgroup := mtag.Get("group")
353
354	if len(subgroup) != 0 {
355		var ptrval reflect.Value
356
357		if realval.Kind() == reflect.Ptr {
358			ptrval = realval
359
360			if ptrval.IsNil() {
361				ptrval.Set(reflect.New(ptrval.Type()))
362			}
363		} else {
364			ptrval = realval.Addr()
365		}
366
367		description := mtag.Get("description")
368
369		group, err := g.AddGroup(subgroup, description, ptrval.Interface())
370
371		if err != nil {
372			return true, err
373		}
374
375		group.Namespace = mtag.Get("namespace")
376		group.EnvNamespace = mtag.Get("env-namespace")
377		group.Hidden = mtag.Get("hidden") != ""
378
379		return true, nil
380	}
381
382	return false, nil
383}
384
385func (g *Group) scanType(handler scanHandler) error {
386	// Get all the public fields in the data struct
387	ptrval := reflect.ValueOf(g.data)
388
389	if ptrval.Type().Kind() != reflect.Ptr {
390		panic(ErrNotPointerToStruct)
391	}
392
393	stype := ptrval.Type().Elem()
394
395	if stype.Kind() != reflect.Struct {
396		panic(ErrNotPointerToStruct)
397	}
398
399	realval := reflect.Indirect(ptrval)
400
401	if err := g.scanStruct(realval, nil, handler); err != nil {
402		return err
403	}
404
405	if err := g.checkForDuplicateFlags(); err != nil {
406		return err
407	}
408
409	return nil
410}
411
412func (g *Group) scan() error {
413	return g.scanType(g.scanSubGroupHandler)
414}
415
416func (g *Group) groupByName(name string) *Group {
417	if len(name) == 0 {
418		return g
419	}
420
421	return g.Find(name)
422}
423