Skip to content
Newer
Older
100644 386 lines (300 sloc) 9.25 KB
462a160 Added license
Jesse van den Kieboom authored Aug 31, 2012
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
c3d1968 Initial import
Jesse van den Kieboom authored Aug 31, 2012
5 package flags
6
7 import (
8 "errors"
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
9 "reflect"
3829c79 Add Group.Find
Jesse van den Kieboom authored Nov 21, 2013
10 "strings"
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
11 "unicode/utf8"
12 "unsafe"
c3d1968 Initial import
Jesse van den Kieboom authored Aug 31, 2012
13 )
14
31be37e @jessevdk Update docs to be more conventional
authored Dec 17, 2013
15 // ErrNotPointerToStruct indicates that a provided data container is not
16 // a pointer to a struct. Only pointers to structs are valid data containers
17 // for options.
c3d1968 Initial import
Jesse van den Kieboom authored Aug 31, 2012
18 var ErrNotPointerToStruct = errors.New("provided data is not a pointer to struct")
cfc3da2 Added documentation
Jesse van den Kieboom authored Aug 31, 2012
19
31be37e @jessevdk Update docs to be more conventional
authored Dec 17, 2013
20 // Group represents an option group. Option groups can be used to logically
21 // group options together under a description. Groups are only used to provide
22 // more structure to options both for the user (as displayed in the help message)
23 // and for you, since groups can be nested.
c3d1968 Initial import
Jesse van den Kieboom authored Aug 31, 2012
24 type Group struct {
31be37e @jessevdk Update docs to be more conventional
authored Dec 17, 2013
25 // A short description of the group. The
78cb5f3 @zimmski correct grammar errors and typos
zimmski authored Mar 12, 2014
26 // short description is primarily used in the built-in generated help
31be37e @jessevdk Update docs to be more conventional
authored Dec 17, 2013
27 // message
28cfab9 Major internal rewrite
Jesse van den Kieboom authored Nov 20, 2013
28 ShortDescription string
479a5be Support command context-sensitive help message
Jesse van den Kieboom authored Dec 1, 2012
29
31be37e @jessevdk Update docs to be more conventional
authored Dec 17, 2013
30 // A long description of the group. The long
31 // description is primarily used to present information on commands
78cb5f3 @zimmski correct grammar errors and typos
zimmski authored Mar 12, 2014
32 // (Command embeds Group) in the built-in generated help and man pages.
28cfab9 Major internal rewrite
Jesse van den Kieboom authored Nov 20, 2013
33 LongDescription string
cfc3da2 Added documentation
Jesse van den Kieboom authored Aug 31, 2012
34
7762684 @zimmski implement group namespaces
zimmski authored May 13, 2014
35 // The namespace of the group
36 Namespace string
37
a15da34 @jessevdk Set and respect Hidden on group
authored Oct 18, 2015
38 // If true, the group is not displayed in the help or man page
39 Hidden bool
40
5b8d63e @zimmski add Group.parent and Option.group
zimmski authored May 13, 2014
41 // The parent of the group or nil if it has no parent
a070a28 @zimmski put Command and Parser objects in the group hierarchy too
zimmski authored May 14, 2014
42 parent interface{}
5b8d63e @zimmski add Group.parent and Option.group
zimmski authored May 13, 2014
43
28cfab9 Major internal rewrite
Jesse van den Kieboom authored Nov 20, 2013
44 // All the options in the group
45 options []*Option
ab5bf80 Implemented support for nested groups
Jesse van den Kieboom authored Sep 4, 2012
46
28cfab9 Major internal rewrite
Jesse van den Kieboom authored Nov 20, 2013
47 // All the subgroups
4751eb0 Run go fmt
Jesse van den Kieboom authored Nov 21, 2013
48 groups []*Group
8f7fbf8 Implemented basic support for commands
Jesse van den Kieboom authored Oct 23, 2012
49
78cb5f3 @zimmski correct grammar errors and typos
zimmski authored Mar 12, 2014
50 // Whether the group represents the built-in help group
0a35daa @jessevdk Do not show builtin help for all groups
authored Jan 10, 2014
51 isBuiltinHelp bool
52
ad53ec4 Ran godoc
Jesse van den Kieboom authored Aug 31, 2012
53 data interface{}
c3d1968 Initial import
Jesse van den Kieboom authored Aug 31, 2012
54 }
55
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
56 type scanHandler func(reflect.Value, *reflect.StructField) (bool, error)
57
28cfab9 Major internal rewrite
Jesse van den Kieboom authored Nov 20, 2013
58 // AddGroup adds a new group to the command with the given name and data. The
59 // data needs to be a pointer to a struct from which the fields indicate which
60 // options are in the group.
61 func (g *Group) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) {
62 group := newGroup(shortDescription, longDescription, data)
8f7fbf8 Implemented basic support for commands
Jesse van den Kieboom authored Oct 23, 2012
63
5b8d63e @zimmski add Group.parent and Option.group
zimmski authored May 13, 2014
64 group.parent = g
65
28cfab9 Major internal rewrite
Jesse van den Kieboom authored Nov 20, 2013
66 if err := group.scan(); err != nil {
67 return nil, err
68 }
b01597c Add Usage interface
Jesse van den Kieboom authored Sep 9, 2013
69
28cfab9 Major internal rewrite
Jesse van den Kieboom authored Nov 20, 2013
70 g.groups = append(g.groups, group)
71 return group, nil
72 }
479a5be Support command context-sensitive help message
Jesse van den Kieboom authored Dec 1, 2012
73
31be37e @jessevdk Update docs to be more conventional
authored Dec 17, 2013
74 // Groups returns the list of groups embedded in this group.
28cfab9 Major internal rewrite
Jesse van den Kieboom authored Nov 20, 2013
75 func (g *Group) Groups() []*Group {
76 return g.groups
77 }
c3d1968 Initial import
Jesse van den Kieboom authored Aug 31, 2012
78
31be37e @jessevdk Update docs to be more conventional
authored Dec 17, 2013
79 // Options returns the list of options in this group.
28cfab9 Major internal rewrite
Jesse van den Kieboom authored Nov 20, 2013
80 func (g *Group) Options() []*Option {
81 return g.options
c3d1968 Initial import
Jesse van den Kieboom authored Aug 31, 2012
82 }
3829c79 Add Group.Find
Jesse van den Kieboom authored Nov 21, 2013
83
31be37e @jessevdk Update docs to be more conventional
authored Dec 17, 2013
84 // Find locates the subgroup with the given short description and returns it.
3829c79 Add Group.Find
Jesse van den Kieboom authored Nov 21, 2013
85 // If no such group can be found Find will return nil. Note that the description
86 // is matched case insensitively.
87 func (g *Group) Find(shortDescription string) *Group {
88 lshortDescription := strings.ToLower(shortDescription)
89
a1f2370 @jessevdk Find groups recursively
authored Nov 21, 2013
90 var ret *Group
91
92 g.eachGroup(func(gg *Group) {
93 if gg != g && strings.ToLower(gg.ShortDescription) == lshortDescription {
94 ret = gg
3829c79 Add Group.Find
Jesse van den Kieboom authored Nov 21, 2013
95 }
cec5830 @jessevdk Remove non-recursive form of eachGroup
authored Nov 21, 2013
96 })
3829c79 Add Group.Find
Jesse van den Kieboom authored Nov 21, 2013
97
a1f2370 @jessevdk Find groups recursively
authored Nov 21, 2013
98 return ret
3829c79 Add Group.Find
Jesse van den Kieboom authored Nov 21, 2013
99 }
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
100
fc93116 @jessevdk Add FindOptionByLongName and FindOptionByShortName
authored Oct 28, 2015
101 func (g *Group) findOption(matcher func(*Option) bool) (option *Option) {
102 g.eachGroup(func(g *Group) {
103 for _, opt := range g.options {
104 if option == nil && matcher(opt) {
105 option = opt
106 }
107 }
108 })
109
110 return option
111 }
112
113 // Find an option that is part of the group, or any of its subgroups,
114 // by matching its long name (including the option namespace).
115 func (g *Group) FindOptionByLongName(longName string) *Option {
116 return g.findOption(func(option *Option) bool {
117 return option.LongNameWithNamespace() == longName
118 })
119 }
120
121 // Find an option that is part of the group, or any of its subgroups,
122 // by matching its short name.
123 func (g *Group) FindOptionByShortName(shortName rune) *Option {
124 return g.findOption(func(option *Option) bool {
125 return option.ShortName == shortName
126 })
127 }
128
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
129 func newGroup(shortDescription string, longDescription string, data interface{}) *Group {
130 return &Group{
131 ShortDescription: shortDescription,
132 LongDescription: longDescription,
133
134 data: data,
135 }
136 }
137
138 func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option {
139 prio := 0
140 var retopt *Option
141
36760f6 @sqs ini: allow specifying group opts directly on subcommands
sqs authored Dec 31, 2015
142 g.eachGroup(func(g *Group) {
143 for _, opt := range g.options {
144 if namematch != nil && namematch(opt, name) && prio < 4 {
145 retopt = opt
146 prio = 4
147 }
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
148
36760f6 @sqs ini: allow specifying group opts directly on subcommands
sqs authored Dec 31, 2015
149 if name == opt.field.Name && prio < 3 {
150 retopt = opt
151 prio = 3
152 }
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
153
36760f6 @sqs ini: allow specifying group opts directly on subcommands
sqs authored Dec 31, 2015
154 if name == opt.LongNameWithNamespace() && prio < 2 {
155 retopt = opt
156 prio = 2
157 }
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
158
36760f6 @sqs ini: allow specifying group opts directly on subcommands
sqs authored Dec 31, 2015
159 if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 {
160 retopt = opt
161 prio = 1
162 }
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
163 }
36760f6 @sqs ini: allow specifying group opts directly on subcommands
sqs authored Dec 31, 2015
164 })
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
165
166 return retopt
167 }
168
169 func (g *Group) eachGroup(f func(*Group)) {
170 f(g)
171
172 for _, gg := range g.groups {
173 gg.eachGroup(f)
174 }
175 }
176
177 func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error {
178 stype := realval.Type()
179
180 if sfield != nil {
181 if ok, err := handler(realval, sfield); err != nil {
182 return err
183 } else if ok {
184 return nil
185 }
186 }
187
188 for i := 0; i < stype.NumField(); i++ {
189 field := stype.Field(i)
190
191 // PkgName is set only for non-exported fields, which we ignore
46e39c7 @sqs Parse unexported embedded option struct fields in Go 1.6 (consistent …
sqs authored Dec 22, 2015
192 if field.PkgPath != "" && !field.Anonymous {
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
193 continue
194 }
195
196 mtag := newMultiTag(string(field.Tag))
197
198 if err := mtag.Parse(); err != nil {
199 return err
200 }
201
202 // Skip fields with the no-flag tag
203 if mtag.Get("no-flag") != "" {
204 continue
205 }
206
207 // Dive deep into structs or pointers to structs
208 kind := field.Type.Kind()
209 fld := realval.Field(i)
210
211 if kind == reflect.Struct {
212 if err := g.scanStruct(fld, &field, handler); err != nil {
213 return err
214 }
215 } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct {
216 if fld.IsNil() {
217 fld.Set(reflect.New(fld.Type().Elem()))
218 }
219
220 if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil {
221 return err
222 }
223 }
224
225 longname := mtag.Get("long")
226 shortname := mtag.Get("short")
227
228 // Need at least either a short or long name
229 if longname == "" && shortname == "" && mtag.Get("ini-name") == "" {
230 continue
231 }
232
233 short := rune(0)
234 rc := utf8.RuneCountInString(shortname)
235
236 if rc > 1 {
237 return newErrorf(ErrShortNameTooLong,
238 "short names can only be 1 character long, not `%s'",
239 shortname)
240
241 } else if rc == 1 {
242 short, _ = utf8.DecodeRuneInString(shortname)
243 }
244
245 description := mtag.Get("description")
246 def := mtag.GetMany("default")
247
248 optionalValue := mtag.GetMany("optional-value")
249 valueName := mtag.Get("value-name")
250 defaultMask := mtag.Get("default-mask")
251
252 optional := (mtag.Get("optional") != "")
253 required := (mtag.Get("required") != "")
254 choices := mtag.GetMany("choice")
255 hidden := (mtag.Get("hidden") != "")
256
257 option := &Option{
258 Description: description,
259 ShortName: short,
260 LongName: longname,
261 Default: def,
262 EnvDefaultKey: mtag.Get("env"),
263 EnvDefaultDelim: mtag.Get("env-delim"),
264 OptionalArgument: optional,
265 OptionalValue: optionalValue,
266 Required: required,
267 ValueName: valueName,
268 DefaultMask: defaultMask,
269 Choices: choices,
270 Hidden: hidden,
271
272 group: g,
273
274 field: field,
275 value: realval.Field(i),
276 tag: mtag,
277 }
278
7f2ab82 @jessevdk Do not allow default tag on boolean flags
authored Feb 27, 2016
279 if option.isBool() && option.Default != nil {
280 return newErrorf(ErrInvalidTag,
6b9493b @jessevdk Fix error message for invalid use of default tag
authored Feb 27, 2016
281 "boolean flag `%s' may not have default values, they always default to `false' and can only be turned on",
7f2ab82 @jessevdk Do not allow default tag on boolean flags
authored Feb 27, 2016
282 option.shortAndLongName())
283 }
284
05e133d @jessevdk Merge _private files
authored Oct 18, 2015
285 g.options = append(g.options, option)
286 }
287
288 return nil
289 }
290
291 func (g *Group) checkForDuplicateFlags() *Error {
292 shortNames := make(map[rune]*Option)
293 longNames := make(map[string]*Option)
294
295 var duplicateError *Error
296
297 g.eachGroup(func(g *Group) {
298 for _, option := range g.options {
299 if option.LongName != "" {
300 longName := option.LongNameWithNamespace()
301
302 if otherOption, ok := longNames[longName]; ok {
303 duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption)
304 return
305 }
306 longNames[longName] = option
307 }
308 if option.ShortName != 0 {
309 if otherOption, ok := shortNames[option.ShortName]; ok {
310 duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption)
311 return
312 }
313 shortNames[option.ShortName] = option
314 }
315 }
316 })
317
318 return duplicateError
319 }
320
321 func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) {
322 mtag := newMultiTag(string(sfield.Tag))
323
324 if err := mtag.Parse(); err != nil {
325 return true, err
326 }
327
328 subgroup := mtag.Get("group")
329
330 if len(subgroup) != 0 {
331 ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr()))
332 description := mtag.Get("description")
333
334 group, err := g.AddGroup(subgroup, description, ptrval.Interface())
335 if err != nil {
336 return true, err
337 }
338
339 group.Namespace = mtag.Get("namespace")
340 group.Hidden = mtag.Get("hidden") != ""
341
342 return true, nil
343 }
344
345 return false, nil
346 }
347
348 func (g *Group) scanType(handler scanHandler) error {
349 // Get all the public fields in the data struct
350 ptrval := reflect.ValueOf(g.data)
351
352 if ptrval.Type().Kind() != reflect.Ptr {
353 panic(ErrNotPointerToStruct)
354 }
355
356 stype := ptrval.Type().Elem()
357
358 if stype.Kind() != reflect.Struct {
359 panic(ErrNotPointerToStruct)
360 }
361
362 realval := reflect.Indirect(ptrval)
363
364 if err := g.scanStruct(realval, nil, handler); err != nil {
365 return err
366 }
367
368 if err := g.checkForDuplicateFlags(); err != nil {
369 return err
370 }
371
372 return nil
373 }
374
375 func (g *Group) scan() error {
376 return g.scanType(g.scanSubGroupHandler)
377 }
378
379 func (g *Group) groupByName(name string) *Group {
380 if len(name) == 0 {
381 return g
382 }
383
384 return g.Find(name)
385 }
Something went wrong with that request. Please try again.