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