-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcommand.go
469 lines (407 loc) · 11.8 KB
/
command.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
package cli
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
)
type Runner interface {
Run() error
}
type ContextRunner interface {
Run(context.Context) error
}
type Beforer interface {
Before() error
}
type Setuper interface {
SetupCommand(cmd *Command)
}
type ExitCoder interface {
ExitCode() int
}
type Command struct {
cli *CLI
name string
help string
description string
config interface{}
helpRequested bool
fields []field
fieldMap map[string]field
argsField *argsField
parent *Command
commands []*Command
commandMap map[string]*Command
}
func (cli *CLI) New(name string, config interface{}, opts ...CommandOption) *Command {
cmd, err := cli.Build(name, config, opts...)
if err != nil {
panic(fmt.Sprintf("cli: %s", err))
}
return cmd
}
func (cli *CLI) Build(name string, config interface{}, opts ...CommandOption) (*Command, error) {
if config == nil {
config = &struct{}{}
}
cmd := &Command{
cli: cli,
name: name,
config: config,
fields: []field{},
fieldMap: map[string]field{},
commands: []*Command{},
commandMap: map[string]*Command{},
}
configFields, argsField, err := cli.getFieldsFromConfig(config)
if err != nil {
return nil, err
}
cmd.argsField = argsField
for _, f := range configFields {
if err := cmd.addField(f, false); err != nil {
return nil, err
}
}
if _, ok := cmd.fieldMap["help"]; !ok {
helpField := field{
Name: "help",
Help: "show usage help",
HasArg: false,
value: &fieldValue{
Setter: &scanfSetter{&cmd.helpRequested},
stringer: staticStringer(""),
isBoolFlag: true,
},
}
if _, ok := cmd.fieldMap["h"]; !ok {
helpField.ShortName = "h"
}
if err := cmd.addField(helpField, true); err != nil {
return nil, err
}
}
if setuper, ok := cmd.config.(Setuper); ok {
setuper.SetupCommand(cmd)
}
for _, opt := range opts {
opt.Apply(cmd)
}
return cmd, nil
}
func (cmd *Command) addField(f field, prepend bool) error {
if prepend {
cmd.fields = append([]field{f}, cmd.fields...)
} else {
cmd.fields = append(cmd.fields, f)
}
if _, ok := cmd.fieldMap[f.Name]; ok {
return fmt.Errorf("multiple fields defined for name: %s", f.Name)
}
cmd.fieldMap[f.Name] = f
if f.ShortName != "" {
if _, ok := cmd.fieldMap[f.ShortName]; ok {
return fmt.Errorf("multiple fields defined for name: %s", f.ShortName)
}
cmd.fieldMap[f.ShortName] = f
}
return nil
}
func (cmd *Command) SetHelp(help string) *Command {
cmd.help = help
return cmd
}
func (cmd *Command) SetDescription(description string) *Command {
cmd.description = description
return cmd
}
// AddCommand registers another Command instance as a subcommand of this Command
// instance.
func (cmd *Command) AddCommand(subCmd *Command) *Command {
if cmd.argsField != nil {
// TODO return error
panic("cli: subcommands cannot be added to a command with an args field")
}
subCmd.parent = cmd
cmd.commands = append(cmd.commands, subCmd)
cmd.commandMap[subCmd.name] = subCmd
return cmd
}
func (cmd *Command) Apply(parent *Command) {
parent.AddCommand(cmd)
}
// Parse is a convenience method for calling ParseArgs(os.Args[1:])
func (cmd *Command) Parse() ParseResult {
return cmd.ParseArgs(os.Args[1:])
}
// ParseArgs parses the passed-in args slice, along with environment variables,
// into the config fields, and returns a ParseResult which can be used for
// further method chaining.
//
// If there are args remaining after parsing this Command's fields, subcommands
// will be recursively parsed until a concrete result is returned
//
// If a Before method is implemented on the config, this method will call it
// before calling Run or recursing into any subcommand parsing.
func (cmd *Command) ParseArgs(args []string) ParseResult {
if args == nil {
args = []string{}
}
r := ParseResult{Command: cmd}
p := parser{fields: cmd.fieldMap, args: args}
// Parse arguments using the flagset.
if err := p.parse(args); err != nil {
return r.err(UsageErrorf("failed to parse args: %w", err))
}
// Return ErrHelp if help was requested.
if cmd.helpRequested {
return r.err(ErrHelp)
}
// Help command
if cmd.parent == nil && cmd.argsField == nil && len(p.args) > 0 && p.args[0] == "help" {
curCmd := cmd
for i := 1; i < len(p.args); i++ {
cmdName := p.args[i]
if subCmd, ok := curCmd.commandMap[cmdName]; ok {
curCmd = subCmd
} else {
return r.err(UsageErrorf("unknown command: %s", cmdName))
}
}
return ParseResult{Command: curCmd, Err: ErrHelp}
}
// Handle remaining arguments so we get unknown command errors before
// invoking Before.
var subCmd *Command
if len(p.args) > 0 {
switch {
case cmd.argsField != nil:
cmd.argsField.setter(p.args)
case len(cmd.commandMap) > 0:
cmdName := p.args[0]
if cmd, ok := cmd.commandMap[cmdName]; ok {
subCmd = cmd
} else {
return r.err(UsageErrorf("unknown command: %s", cmdName))
}
default:
return r.err(UsageErrorf("command does not take arguments"))
}
}
// Parse environment variables.
if err := cmd.parseEnvVars(); err != nil {
return r.err(UsageErrorf("failed to parse environment variables: %w", err))
}
// Return an error if any required fields were not set at least once.
if err := cmd.checkRequired(); err != nil {
return r.err(UsageError(err))
}
// If the config implements a Before method, run it before we recursively
// parse subcommands.
if beforer, ok := cmd.config.(Beforer); ok {
if err := beforer.Before(); err != nil {
return r.err(err)
}
}
// Recursive to subcommand parsing, if applicable.
if subCmd != nil {
return subCmd.ParseArgs(p.args[1:])
}
r.runFunc = getRunFunc(cmd.config)
if r.runFunc == nil && len(cmd.commands) != 0 {
return r.err(UsageErrorf("no command specified"))
}
return r
}
type runFunc struct {
run func(context.Context) error
supportsContext bool
}
func getRunFunc(config interface{}) *runFunc {
if r, ok := config.(Runner); ok {
run := func(context.Context) error {
return r.Run()
}
return &runFunc{
run: run,
supportsContext: false,
}
}
if r, ok := config.(ContextRunner); ok {
return &runFunc{
run: r.Run,
supportsContext: true,
}
}
return nil
}
// parseEnvVars sets any unset field values using the environment variable
// matching the "env" tag of the field, if present.
func (cmd *Command) parseEnvVars() error {
for _, f := range cmd.fields {
if f.EnvVarName == "" || f.value.setCount > 0 {
continue
}
val, ok, err := cmd.cli.LookupEnv(f.EnvVarName)
if err != nil {
// TODO?
return err
}
if ok {
if err := f.value.Set(val); err != nil {
return fmt.Errorf("error parsing %s: %w", f.EnvVarName, err)
}
}
}
return nil
}
// checkRequired returns an error if any fields are required but have not been set.
func (cmd *Command) checkRequired() error {
for _, f := range cmd.fields {
if f.Required && f.value.setCount < 1 {
return fmt.Errorf("required flag %s not set", f.Name)
}
}
return nil
}
// UsageError wraps the given error as a UsageErrorWrapper.
func UsageError(err error) UsageErrorWrapper {
return UsageErrorWrapper{Err: err}
}
// UsageErrorf is a convenience method for wrapping the result of fmt.Errorf as
// a UsageErrorWrapper.
func UsageErrorf(format string, v ...interface{}) UsageErrorWrapper {
return UsageErrorWrapper{Err: fmt.Errorf(format, v...)}
}
// UsageErrorWrapper wraps another error to indicate that the error was due to
// incorrect usage. When this error is handled, help text should be printed in
// addition to the error message.
type UsageErrorWrapper struct {
Err error
}
func (w UsageErrorWrapper) Unwrap() error {
return w.Err
}
func (w UsageErrorWrapper) Error() string {
return w.Err.Error()
}
// ParseResult contains information about the results of command argument
// parsing.
type ParseResult struct {
Err error
Command *Command
runFunc *runFunc
}
// Convenience method for returning errors wrapped as a ParsedResult.
func (r ParseResult) err(err error) ParseResult {
r.Err = err
return r
}
func (r ParseResult) writeHelpIfUsageOrHelpError(err error) {
if err == nil || r.Command == nil || r.Command.cli.HelpWriter == nil {
return
}
_, isUsageErr := err.(UsageErrorWrapper)
if isUsageErr || err == ErrHelp {
r.Command.WriteHelp(r.Command.cli.HelpWriter)
}
}
// Run calls the Run method of the Command config for the parsed command or, if
// an error occurred during parsing, prints the help text and returns that
// error instead. If help was requested, the error will flag.ErrHelp. If the
// underlying command Run method accepts a context, context.Background() will
// be passed.
func (r ParseResult) Run() error {
return r.RunWithContext(context.Background())
}
// RunWithContext is like Run, but it accepts an explicit context which will be
// passed to the command's Run method, if it accepts one.
func (r ParseResult) RunWithContext(ctx context.Context) error {
if r.Err != nil {
r.writeHelpIfUsageOrHelpError(r.Err)
return r.Err
}
if r.runFunc == nil {
return fmt.Errorf("no run method implemented")
}
if err := r.runFunc.run(ctx); err != nil {
r.writeHelpIfUsageOrHelpError(err)
return err
}
return nil
}
// RunWithSigCancel is like Run, but it automatically registers a signal
// handler for SIGINT and SIGTERM that will cancel the context that is passed
// to the command's Run method, if it accepts one.
func (r ParseResult) RunWithSigCancel() error {
ctx, stop := r.contextWithSigCancelIfSupported(context.Background())
defer stop()
return r.RunWithContext(ctx)
}
// RunFatal is like Run, except it automatically handles printing out any
// errors returned by the Run method of the underlying Command config, and
// exits with an appropriate status code.
//
// If no error occurs, the exit code will be 0. If an error is returned and it
// implements the ExitCoder interface, the result of ExitCode() will be used as
// the exit code. If an error is returned that does not implement ExitCoder,
// the exit code will be 1.
func (r ParseResult) RunFatal() {
r.RunFatalWithContext(context.Background())
}
// RunFatalWithContext is like RunFatal, but it accepts an explicit context
// which will be passed to the command's Run method if it accepts one.
func (r ParseResult) RunFatalWithContext(ctx context.Context) {
err := r.RunWithContext(ctx)
if err != nil {
if err != ErrHelp && r.Command != nil && r.Command.cli.ErrWriter != nil {
fmt.Fprintf(r.Command.cli.ErrWriter, "error: %s\n", err)
}
if ec, ok := err.(ExitCoder); ok {
os.Exit(ec.ExitCode())
}
os.Exit(1)
}
os.Exit(0)
}
// RunFatalWithSigCancel is like RunFatal, but it automatically registers a
// signal handler for SIGINT and SIGTERM that will cancel the context that is
// passed to the command's Run method, if it accepts one.
func (r ParseResult) RunFatalWithSigCancel() {
ctx, stop := r.contextWithSigCancelIfSupported(context.Background())
defer stop()
r.RunFatalWithContext(ctx)
}
func (r ParseResult) contextWithSigCancelIfSupported(ctx context.Context) (context.Context, context.CancelFunc) {
if r.runFunc == nil || !r.runFunc.supportsContext {
return ctx, func() {}
}
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
go func() {
// Cancel the signal notify on the first signal so that subsequent
// SIGINT/SIGTERM immediately interrupt the program using the usual go
// runtime handling.
<-ctx.Done()
cancel()
}()
return ctx, cancel
}
type CommandOption interface {
Apply(cmd *Command)
}
type commandOptionFunc func(cmd *Command)
func (of commandOptionFunc) Apply(cmd *Command) {
of(cmd)
}
func WithHelp(help string) CommandOption {
return commandOptionFunc(func(cmd *Command) {
cmd.SetHelp(help)
})
}
func WithDescription(description string) CommandOption {
return commandOptionFunc(func(cmd *Command) {
cmd.SetDescription(description)
})
}