-
Notifications
You must be signed in to change notification settings - Fork 2
/
gommander.go
645 lines (534 loc) · 16.8 KB
/
gommander.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
// Package gommander is an easily-extensible commander package for easily creating Command Line Interfaces.
// View the README for getting started documentation:
package gommander
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
var cache = make(map[string]bool, 0)
func clearCache() {
for k := range cache {
delete(cache, k)
}
}
type CommandCallback = func(*ParserMatches)
type Command struct {
aliases []string
arguments []*Argument
author string
callback CommandCallback
discussion string
emitter EventEmitter
flags []*Flag
help string
isRoot bool
name string
options []*Option
parent *Command
subCommands []*Command
settings AppSettings
globalSettings *AppSettings
theme Theme
version string
usageStr string
customUsageStr string
subCmdGroups map[string][]*Command
appRef *Command
subCmdsHelpHeading string
subCmdsHelpValue string
flagsHelpHeading string
flagsHelpValue string
optionsHelpHeading string
optionsHelpValue string
argsHelpHeading string
argsHelpValue string
}
func App() *Command {
app := NewCommand("")
app.isRoot = true
app.flags = append(app.flags, versionFlag())
app.theme = DefaultTheme()
return app
}
func NewCommand(name string) *Command {
return &Command{
name: name,
flags: []*Flag{helpFlag()},
settings: AppSettings{},
isRoot: false,
emitter: newEmitter(),
globalSettings: &AppSettings{},
usageStr: name,
subCmdGroups: make(map[string][]*Command),
subCmdsHelpHeading: "SUBCOMMANDS",
subCmdsHelpValue: "<SUBCOMMAND>",
flagsHelpHeading: "FLAGS",
flagsHelpValue: "[FLAG]",
optionsHelpHeading: "OPTIONS",
optionsHelpValue: "[OPTION]",
argsHelpHeading: "ARGS",
argsHelpValue: "[ARG]",
}
}
/****************************** Value Getters ****************************/
// Simply returns the alias of the command or an empty string
func (c *Command) GetAliases() []string { return c.aliases }
// Returns the author of the program if one is set
func (c *Command) GetAuthor() string { return c.author }
// Returns a slice of the configured arguments for a command
func (c *Command) GetArguments() []*Argument { return c.arguments }
// Returns a slice of the configured flags
func (c *Command) GetFlags() []*Flag { return c.flags }
// Returns the help string / description that gets printed out on help
func (c *Command) GetHelp() string { return c.help }
// Returns the configured name of a command
func (c *Command) GetName() string { return c.name }
// Returns the slice of options belonging to a command
func (c *Command) GetOptions() []*Option { return c.options }
// Returns the parent of a command or nil if none is found
func (c *Command) GetParent() *Command { return c.parent }
// Returns a slice of subcommands chained to the command instance
func (c *Command) GetSubCommands() []*Command { return c.subCommands }
// Returns the version of the program
func (c *Command) GetVersion() string { return c.version }
// Returns the default usage string or a custom_usage_str if one exists
func (c *Command) GetUsageStr() string {
if len(c.customUsageStr) > 0 {
return c.customUsageStr
}
return c.usageStr
}
/****************************** Command Metadata Setters ****************************/
// This method set the callback to be excuted when a command is matched
func (c *Command) Action(cb CommandCallback) *Command {
c.callback = cb
return c
}
// A method for adding a flag to a command. It is similar to the `.Flag()` method except this method receives an instance of an already created flag while `.Flag()` receives a string, creates a flag from it and call this method internally
func (c *Command) AddFlag(flag *Flag) *Command {
id := fmt.Sprintf("flag-%s", flag.Name)
if !cache[id] {
cache[id] = true
c.flags = append(c.flags, flag)
}
return c
}
// A method for adding a new option to a command. The `.Option()` method invokes this one internally. Identical to the `.AddFlag()` method except this one is for options instead of flags
func (c *Command) AddOption(opt *Option) *Command {
id := fmt.Sprintf("option-%s", opt.Name)
if !cache[id] {
cache[id] = true
c.options = append(c.options, opt)
}
return c
}
// Simply sets the alias of a command
func (c *Command) Alias(alias string) *Command {
c.aliases = append(c.aliases, alias)
return c
}
func (c *Command) AddArgument(arg *Argument) *Command {
id := fmt.Sprintf("arg-%s", arg.Name)
if !cache[id] {
cache[id] = true
c.arguments = append(c.arguments, arg)
}
return c
}
// A method for setting any expected arguments for a command, it takes in the value of the argument e.g. `<image-name>` and the help string for said argument
func (c *Command) Argument(val string, help string) *Command {
argument := newArgument(val, help)
c.AddArgument(argument)
return c
}
// Simply sets the author of the program, usually invoked on the root command
func (c *Command) Author(val string) *Command {
c.author = val
return c
}
// Receives a string representing the flag structure and the flag help string and creates a new flag from it. Acceptable values include:
// ("-h --help", "A help flag")
// You could also omit the short or long version of the flag
func (c *Command) Flag(val string, help string) *Command {
flag := newFlag(val, help)
return c.AddFlag(&flag)
}
// Used to set more information or the command discussion which gets printed out when help is invoked, at the bottom most section
func (c *Command) Discussion(info string) *Command {
c.discussion = info
return c
}
// Simply sets the help string, otherwise known as description of a command
func (c *Command) Help(help string) *Command {
c.help = help
return c
}
// Sets the name of a command, and updates the usage str as well
func (c *Command) Name(name string) *Command {
c.name = name
c.usageStr = name
return c
}
// Sets the version of a command, usually the entry point command(App)
func (c *Command) Version(version string) *Command {
c.version = version
return c
}
// An identical method to the `.Flag()` method but for options. Expected syntax: "-p --port <port-number>"
func (c *Command) Option(val string, help string) *Command {
option := newOption(val, help, false)
return c.AddOption(&option)
}
// This method is used to mark an option as required for a given command. Another way of achieving this is using the `.AddOption()` method and using the `NewOption()` builder interface to define option parameters
func (c *Command) RequiredOption(val string, help string) *Command {
opt := newOption(val, help, true)
return c.AddOption(&opt)
}
// Used to define a custom usage string. If one is present, it will be used instead of the default one
func (c *Command) UsageStr(val string) *Command {
c.customUsageStr = val
return c
}
func (c *Command) SubCmdsHelpHeading(val string) *Command {
c.subCmdsHelpHeading = val
return c
}
func (c *Command) FlagsHelpHeading(val string) *Command {
c.flagsHelpHeading = val
return c
}
func (c *Command) OptionsHelpHeading(val string) *Command {
c.optionsHelpHeading = val
return c
}
func (c *Command) ArgsHelpHeading(val string) *Command {
c.argsHelpHeading = val
return c
}
func (c *Command) SubCmdsHelpValue(val string) *Command {
c.subCmdsHelpValue = val
return c
}
func (c *Command) FlagsHelpValue(val string) *Command {
c.flagsHelpValue = val
return c
}
func (c *Command) OptionsHelpValue(val string) *Command {
c.optionsHelpValue = val
return c
}
func (c *Command) ArgsHelpValue(val string) *Command {
c.argsHelpValue = val
return c
}
/****************************** Subcommand related methods ****************************/
// When chained on a command, this method adds said command to the provided sub_cmd group in the parent of the command.
func (c *Command) AddToGroup(name string) *Command {
c.parent.subCmdGroups[name] = append(c.parent.subCmdGroups[name], c)
return c
}
// Receives a reference to a command, sets the command parent and usage string then adds its to the slice of subcommands. This method is called internally by the `.SubCommand()` method but users can also invoke it directly
func (c *Command) AddSubCommand(subCmd *Command) *Command {
id := fmt.Sprintf("subcmd-%s", subCmd.name)
if !cache[id] {
cache[id] = true
subCmd.parent = c
c.subCommands = append(c.subCommands, subCmd)
cmdPath := []string{c.usageStr, subCmd.usageStr}
subCmd.usageStr = strings.Join(cmdPath, " ")
// Propagate global flags to children
for _, f := range c.GetFlags() {
if f.IsGlobal {
subCmd.AddFlag(f)
}
}
// propagate theme
subCmd.theme = c.theme
if c.isRoot {
subCmd.appRef = c
} else {
subCmd.appRef = c.appRef
}
}
return c
}
// An easier method for creating sub_cmds while avoiding too much function paramets nesting. It accepts the name of the new sub_cmd and returns the newly created sub_cmd
func (c *Command) SubCommand(name string) *Command {
subCmd := NewCommand(name)
c.AddSubCommand(subCmd)
return subCmd
}
// A manual way of creating a new subcommand group and adding the desired commands to it
func (c *Command) SubCommandGroup(name string, vals []*Command) {
c.subCmdGroups[name] = append(c.subCmdGroups[name], vals...)
}
/****************************** Settings ****************************/
func (c *Command) _init() {
if c.settings[DisableVersionFlag] {
c.removeFlag("--version")
}
if c.settings[IncludeHelpSubcommand] && len(c.subCommands) > 0 {
validSubcmds := []string{}
for _, c := range c.subCommands {
validSubcmds = append(validSubcmds, c.name)
}
c.SubCommand("help").
Help("Print out help information for the passed command").
AddArgument(
NewArgument("<COMMAND>").
Help("The name of the command to output help for").
ValidateWith(validSubcmds),
).
Action(func(pm *ParserMatches) {
val, _ := pm.GetArgValue("<COMMAND>")
parent := pm.matchedCmd.parent
if parent != nil {
cmd, _ := parent.findSubcommand(val)
cmd.PrintHelp()
}
})
}
// Default help listener cannot be overridden
c.emitter.on(OutputHelp, func(ec *EventConfig) {
cmd := ec.matchedCmd
cmd.PrintHelp()
}, -4)
if !c.settings[OverrideAllDefaultListeners] {
c.emitter.onErrors(func(ec *EventConfig) {
err := ec.err
// TODO: Match theme in better way
err.Display(c)
})
c.emitter.on(OutputVersion, func(ec *EventConfig) {
// TODO: Print version in a better way
app := ec.appRef
fmt.Println(app.GetName(), app.GetVersion())
fmt.Println(app.GetAuthor())
fmt.Println(app.GetHelp())
}, -4)
for _, event := range c.emitter.eventsToOverride {
c.emitter.rmDefaultLstnr(event)
}
}
}
// A method for configuring the settings of a command
func (c *Command) Set(s Setting, value bool) *Command {
c.settings[s] = value
return c
}
// A method for configuring the theme of a command
func (c *Command) Theme(value Theme) *Command {
c.theme = value
return c
}
// A method for configuring a command to use a package-predefined theme when printing output
func (c *Command) UsePredefinedTheme(value PredefinedTheme) *Command {
c.Theme(GetPredefinedTheme(value))
return c
}
/****************************** Parser Functionality ****************************/
func (c *Command) _isExpectingValues() bool {
hasDefaults := func(list []*Argument) bool {
for _, a := range c.arguments {
if a.hasDefaultValue() {
} else {
return false
}
}
return true
}
return len(c.subCommands) > 0 || (len(c.arguments) > 0 && !hasDefaults(c.arguments))
}
func (c *Command) _parse(vals []string) {
// TODO: Init/build the commands- set default listeners, add help subcmd, sync settings
c._init()
c._setBinName(vals[0])
rawArgs := vals[1:]
parser := NewParser(c)
matches, err := parser.parse(rawArgs)
if err != nil {
event := EventConfig{
err: *err,
args: err.args,
event: err.kind,
exitCode: err.exitCode,
appRef: c,
matchedCmd: matches.matchedCmd,
}
c.emit(event)
}
// TODO: No errors, check special flags
matchedCmd := matches.GetMatchedCommand()
cmdIdx := matches.GetMatchedCommandIndex()
// Check special flags
// TODO: Sync with program settings
if matches.ContainsFlag("help") {
event := EventConfig{
event: OutputHelp,
exitCode: 0,
appRef: c,
matchedCmd: matchedCmd,
}
c.emit(event)
} else if matches.ContainsFlag("version") {
event := EventConfig{
event: OutputVersion,
exitCode: 0,
appRef: c,
matchedCmd: matchedCmd,
}
c.emit(event)
}
showHelp := func() {
if !isTestMode() {
matchedCmd.PrintHelp()
}
}
if matchedCmd.callback != nil {
// No args passed to the matched cmd
if cmdIdx == -1 {
cmdIdx++
}
if (len(rawArgs) == 0 || len(matches.rawArgs[cmdIdx:]) == 0) && matchedCmd._isExpectingValues() {
showHelp()
return
}
// Invoke callback
matchedCmd.callback(matches)
} else {
showHelp()
}
}
// A method for parsing the arguments passed to a program and invoking the callback on a command if one is found. This method also handles any errors encountered while parsing.
func (c *Command) Parse() {
c._parse(os.Args)
}
func (c *Command) ParseFrom(args []string) {
c._parse(args)
}
/****************************** Event emitter functionality ****************************/
// Makes a call to the Command event emitter to `emit` a new event from the passed config
func (c *Command) emit(cfg EventConfig) {
c.emitter.emit(cfg)
}
// Used to add a new listener for a specific event which gets triggered when the event occurs
func (c *Command) On(event Event, cb EventCallback) {
c.emitter.on(event, cb, 0)
}
// This method is also used to add a new listener to a specific event but also overrides the default listener created by the package for said event
func (c *Command) Override(event Event, cb EventCallback) {
c.emitter.override(event)
c.emitter.on(event, cb, 0)
}
// A method for setting a listener that gets executed after all events encountered in the program
func (c *Command) AfterAll(cb EventCallback) {
c.emitter.insertAfterAll(cb)
}
// Set a callback to be executed only after the help event
func (c *Command) AfterHelp(cb EventCallback) {
c.emitter.on(OutputHelp, cb, 4)
}
// Set a callback to be executed before all events encountered
func (c *Command) BeforeAll(cb EventCallback) {
c.emitter.insertBeforeAll(cb)
}
// Set a callback to be executed only before the help event
func (c *Command) BeforeHelp(cb EventCallback) {
c.emitter.on(OutputHelp, cb, -4)
}
/****************************** Other Command Utilities ****************************/
func (c *Command) hasSubcommands() bool {
return len(c.subCommands) > 0
}
func (c *Command) findSubcommand(val string) (*Command, error) {
for _, sc := range c.subCommands {
includes := func(val string) bool {
for _, v := range sc.aliases {
if v == val {
return true
}
}
return false
}
if sc.name == val || includes(val) {
return sc, nil
}
}
return NewCommand(""), errors.New("no such subcmd")
}
func (c *Command) suggestSubCmd(val string) []string {
var minMatchSize = 3
var matches []string
cmdMap := make(map[string]int, 0)
for _, v := range c.subCommands {
cmdMap[v.name] = 0
}
for _, sc := range c.subCommands {
for i, v := range strings.Split(val, "") {
if len(sc.name) > i {
var next string
current := string(sc.name[i])
if len(sc.name) > i+1 {
next = string(sc.name[i+1])
}
if next == v || current == v {
cmdMap[sc.name] += 1
}
}
}
}
for k, v := range cmdMap {
if v >= minMatchSize {
matches = append(matches, k)
}
}
return matches
}
func (c *Command) removeFlag(val string) {
newFlags := []*Flag{}
for _, f := range c.flags {
if !(f.ShortVal == val || f.LongVal == val) {
newFlags = append(newFlags, f)
}
}
c.flags = newFlags
}
func (c *Command) _getAppRef() *Command {
if c.isRoot {
return c
}
return c.appRef
}
func (c *Command) _getUsageStr() string {
var newUsage strings.Builder
if len(c.customUsageStr) > 0 {
if !strings.Contains(c.customUsageStr, c.parent.usageStr) {
newUsage.WriteString(c.parent.usageStr)
newUsage.WriteRune(' ')
}
newUsage.WriteString(c.customUsageStr)
} else {
if c.parent != nil && c.parent.isRoot && !strings.Contains(c.usageStr, c.parent.usageStr) {
newUsage.WriteString(c.parent.usageStr)
}
newUsage.WriteString(c.usageStr)
}
return newUsage.String()
}
func (c *Command) _setBinName(val string) {
if len(c.name) == 0 {
binName := filepath.Base(val)
// TODO: Validation
c.Name(binName)
}
}
func (c *Command) PrintHelp() {
HelpWriter{}.Write(c)
}
/****************************** Interface Implementations ****************************/
func (c *Command) generate(app *Command) (string, string) {
return c.GetName(), c.GetHelp()
}