blob: 486bacba1ffeabeea747bdaa5af9d06f287ab04d [file] [log] [blame]
Scott Bakerbdb962b2020-04-03 10:53:36 -07001package flags
2
3import (
4 "reflect"
5 "sort"
6 "strconv"
7 "strings"
8)
9
10// Command represents an application command. Commands can be added to the
11// parser (which itself is a command) and are selected/executed when its name
12// is specified on the command line. The Command type embeds a Group and
13// therefore also carries a set of command specific options.
14type Command struct {
15 // Embedded, see Group for more information
16 *Group
17
18 // The name by which the command can be invoked
19 Name string
20
21 // The active sub command (set by parsing) or nil
22 Active *Command
23
24 // Whether subcommands are optional
25 SubcommandsOptional bool
26
27 // Aliases for the command
28 Aliases []string
29
30 // Whether positional arguments are required
31 ArgsRequired bool
32
33 commands []*Command
34 hasBuiltinHelpGroup bool
35 args []*Arg
36}
37
38// Commander is an interface which can be implemented by any command added in
39// the options. When implemented, the Execute method will be called for the last
40// specified (sub)command providing the remaining command line arguments.
41type Commander interface {
42 // Execute will be called for the last active (sub)command. The
43 // args argument contains the remaining command line arguments. The
44 // error that Execute returns will be eventually passed out of the
45 // Parse method of the Parser.
46 Execute(args []string) error
47}
48
49// Usage is an interface which can be implemented to show a custom usage string
50// in the help message shown for a command.
51type Usage interface {
52 // Usage is called for commands to allow customized printing of command
53 // usage in the generated help message.
54 Usage() string
55}
56
57type lookup struct {
58 shortNames map[string]*Option
59 longNames map[string]*Option
60
61 commands map[string]*Command
62}
63
64// AddCommand adds a new command to the parser with the given name and data. The
65// data needs to be a pointer to a struct from which the fields indicate which
66// options are in the command. The provided data can implement the Command and
67// Usage interfaces.
68func (c *Command) AddCommand(command string, shortDescription string, longDescription string, data interface{}) (*Command, error) {
69 cmd := newCommand(command, shortDescription, longDescription, data)
70
71 cmd.parent = c
72
73 if err := cmd.scan(); err != nil {
74 return nil, err
75 }
76
77 c.commands = append(c.commands, cmd)
78 return cmd, nil
79}
80
81// AddGroup adds a new group to the command with the given name and data. The
82// data needs to be a pointer to a struct from which the fields indicate which
83// options are in the group.
84func (c *Command) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) {
85 group := newGroup(shortDescription, longDescription, data)
86
87 group.parent = c
88
89 if err := group.scanType(c.scanSubcommandHandler(group)); err != nil {
90 return nil, err
91 }
92
93 c.groups = append(c.groups, group)
94 return group, nil
95}
96
97// Commands returns a list of subcommands of this command.
98func (c *Command) Commands() []*Command {
99 return c.commands
100}
101
102// Find locates the subcommand with the given name and returns it. If no such
103// command can be found Find will return nil.
104func (c *Command) Find(name string) *Command {
105 for _, cc := range c.commands {
106 if cc.match(name) {
107 return cc
108 }
109 }
110
111 return nil
112}
113
114// FindOptionByLongName finds an option that is part of the command, or any of
115// its parent commands, by matching its long name (including the option
116// namespace).
117func (c *Command) FindOptionByLongName(longName string) (option *Option) {
118 for option == nil && c != nil {
119 option = c.Group.FindOptionByLongName(longName)
120
121 c, _ = c.parent.(*Command)
122 }
123
124 return option
125}
126
127// FindOptionByShortName finds an option that is part of the command, or any of
128// its parent commands, by matching its long name (including the option
129// namespace).
130func (c *Command) FindOptionByShortName(shortName rune) (option *Option) {
131 for option == nil && c != nil {
132 option = c.Group.FindOptionByShortName(shortName)
133
134 c, _ = c.parent.(*Command)
135 }
136
137 return option
138}
139
140// Args returns a list of positional arguments associated with this command.
141func (c *Command) Args() []*Arg {
142 ret := make([]*Arg, len(c.args))
143 copy(ret, c.args)
144
145 return ret
146}
147
148func newCommand(name string, shortDescription string, longDescription string, data interface{}) *Command {
149 return &Command{
150 Group: newGroup(shortDescription, longDescription, data),
151 Name: name,
152 }
153}
154
155func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler {
156 f := func(realval reflect.Value, sfield *reflect.StructField) (bool, error) {
157 mtag := newMultiTag(string(sfield.Tag))
158
159 if err := mtag.Parse(); err != nil {
160 return true, err
161 }
162
163 positional := mtag.Get("positional-args")
164
165 if len(positional) != 0 {
166 stype := realval.Type()
167
168 for i := 0; i < stype.NumField(); i++ {
169 field := stype.Field(i)
170
171 m := newMultiTag((string(field.Tag)))
172
173 if err := m.Parse(); err != nil {
174 return true, err
175 }
176
177 name := m.Get("positional-arg-name")
178
179 if len(name) == 0 {
180 name = field.Name
181 }
182
183 required := -1
184 requiredMaximum := -1
185
186 sreq := m.Get("required")
187
188 if sreq != "" {
189 required = 1
190
191 rng := strings.SplitN(sreq, "-", 2)
192
193 if len(rng) > 1 {
194 if preq, err := strconv.ParseInt(rng[0], 10, 32); err == nil {
195 required = int(preq)
196 }
197
198 if preq, err := strconv.ParseInt(rng[1], 10, 32); err == nil {
199 requiredMaximum = int(preq)
200 }
201 } else {
202 if preq, err := strconv.ParseInt(sreq, 10, 32); err == nil {
203 required = int(preq)
204 }
205 }
206 }
207
208 arg := &Arg{
209 Name: name,
210 Description: m.Get("description"),
211 Required: required,
212 RequiredMaximum: requiredMaximum,
213
214 value: realval.Field(i),
215 tag: m,
216 }
217
218 c.args = append(c.args, arg)
219
220 if len(mtag.Get("required")) != 0 {
221 c.ArgsRequired = true
222 }
223 }
224
225 return true, nil
226 }
227
228 subcommand := mtag.Get("command")
229
230 if len(subcommand) != 0 {
231 var ptrval reflect.Value
232
233 if realval.Kind() == reflect.Ptr {
234 ptrval = realval
235
236 if ptrval.IsNil() {
237 ptrval.Set(reflect.New(ptrval.Type().Elem()))
238 }
239 } else {
240 ptrval = realval.Addr()
241 }
242
243 shortDescription := mtag.Get("description")
244 longDescription := mtag.Get("long-description")
245 subcommandsOptional := mtag.Get("subcommands-optional")
246 aliases := mtag.GetMany("alias")
247
248 subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface())
249
250 if err != nil {
251 return true, err
252 }
253
254 subc.Hidden = mtag.Get("hidden") != ""
255
256 if len(subcommandsOptional) > 0 {
257 subc.SubcommandsOptional = true
258 }
259
260 if len(aliases) > 0 {
261 subc.Aliases = aliases
262 }
263
264 return true, nil
265 }
266
267 return parentg.scanSubGroupHandler(realval, sfield)
268 }
269
270 return f
271}
272
273func (c *Command) scan() error {
274 return c.scanType(c.scanSubcommandHandler(c.Group))
275}
276
277func (c *Command) eachOption(f func(*Command, *Group, *Option)) {
278 c.eachCommand(func(c *Command) {
279 c.eachGroup(func(g *Group) {
280 for _, option := range g.options {
281 f(c, g, option)
282 }
283 })
284 }, true)
285}
286
287func (c *Command) eachCommand(f func(*Command), recurse bool) {
288 f(c)
289
290 for _, cc := range c.commands {
291 if recurse {
292 cc.eachCommand(f, true)
293 } else {
294 f(cc)
295 }
296 }
297}
298
299func (c *Command) eachActiveGroup(f func(cc *Command, g *Group)) {
300 c.eachGroup(func(g *Group) {
301 f(c, g)
302 })
303
304 if c.Active != nil {
305 c.Active.eachActiveGroup(f)
306 }
307}
308
309func (c *Command) addHelpGroups(showHelp func() error) {
310 if !c.hasBuiltinHelpGroup {
311 c.addHelpGroup(showHelp)
312 c.hasBuiltinHelpGroup = true
313 }
314
315 for _, cc := range c.commands {
316 cc.addHelpGroups(showHelp)
317 }
318}
319
320func (c *Command) makeLookup() lookup {
321 ret := lookup{
322 shortNames: make(map[string]*Option),
323 longNames: make(map[string]*Option),
324 commands: make(map[string]*Command),
325 }
326
327 parent := c.parent
328
329 var parents []*Command
330
331 for parent != nil {
332 if cmd, ok := parent.(*Command); ok {
333 parents = append(parents, cmd)
334 parent = cmd.parent
335 } else {
336 parent = nil
337 }
338 }
339
340 for i := len(parents) - 1; i >= 0; i-- {
341 parents[i].fillLookup(&ret, true)
342 }
343
344 c.fillLookup(&ret, false)
345 return ret
346}
347
348func (c *Command) fillLookup(ret *lookup, onlyOptions bool) {
349 c.eachGroup(func(g *Group) {
350 for _, option := range g.options {
351 if option.ShortName != 0 {
352 ret.shortNames[string(option.ShortName)] = option
353 }
354
355 if len(option.LongName) > 0 {
356 ret.longNames[option.LongNameWithNamespace()] = option
357 }
358 }
359 })
360
361 if onlyOptions {
362 return
363 }
364
365 for _, subcommand := range c.commands {
366 ret.commands[subcommand.Name] = subcommand
367
368 for _, a := range subcommand.Aliases {
369 ret.commands[a] = subcommand
370 }
371 }
372}
373
374func (c *Command) groupByName(name string) *Group {
375 if grp := c.Group.groupByName(name); grp != nil {
376 return grp
377 }
378
379 for _, subc := range c.commands {
380 prefix := subc.Name + "."
381
382 if strings.HasPrefix(name, prefix) {
383 if grp := subc.groupByName(name[len(prefix):]); grp != nil {
384 return grp
385 }
386 } else if name == subc.Name {
387 return subc.Group
388 }
389 }
390
391 return nil
392}
393
394type commandList []*Command
395
396func (c commandList) Less(i, j int) bool {
397 return c[i].Name < c[j].Name
398}
399
400func (c commandList) Len() int {
401 return len(c)
402}
403
404func (c commandList) Swap(i, j int) {
405 c[i], c[j] = c[j], c[i]
406}
407
408func (c *Command) sortedVisibleCommands() []*Command {
409 ret := commandList(c.visibleCommands())
410 sort.Sort(ret)
411
412 return []*Command(ret)
413}
414
415func (c *Command) visibleCommands() []*Command {
416 ret := make([]*Command, 0, len(c.commands))
417
418 for _, cmd := range c.commands {
419 if !cmd.Hidden {
420 ret = append(ret, cmd)
421 }
422 }
423
424 return ret
425}
426
427func (c *Command) match(name string) bool {
428 if c.Name == name {
429 return true
430 }
431
432 for _, v := range c.Aliases {
433 if v == name {
434 return true
435 }
436 }
437
438 return false
439}
440
441func (c *Command) hasCliOptions() bool {
442 ret := false
443
444 c.eachGroup(func(g *Group) {
445 if g.isBuiltinHelp {
446 return
447 }
448
449 for _, opt := range g.options {
450 if opt.canCli() {
451 ret = true
452 }
453 }
454 })
455
456 return ret
457}
458
459func (c *Command) fillParseState(s *parseState) {
460 s.positional = make([]*Arg, len(c.args))
461 copy(s.positional, c.args)
462
463 s.lookup = c.makeLookup()
464 s.command = c
465}