-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcommand.go
More file actions
623 lines (580 loc) · 24 KB
/
Copy pathcommand.go
File metadata and controls
623 lines (580 loc) · 24 KB
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
package climux
import (
"context"
"encoding/json"
"slices"
"go.hotsrc.dev/climux/desc"
"go.hotsrc.dev/climux/internal/argv"
"go.hotsrc.dev/climux/ir"
)
// An Invocation is the result of parsing a command line. It records which
// command the arguments named, what was left for its handler, and the
// streams the handler should read and write.
type Invocation = ir.Invocation
// A HandlerFunc runs a command. Register one with Command.HandleFunc.
//
// ctx is the context given to Run, so a handler that does anything
// cancelable should honor it.
//
// inv describes how the command was called: which command was named, any
// arguments forwarded past a "--" terminator, and the streams to use. A
// handler should write inv.Stdout and inv.Stderr rather than the process
// streams, so that a caller redirecting the command captures its output.
//
// Returning nil exits 0 and returning an error exits 1, unless the error
// implements ExitCoder. See RunWithArgs.
type HandlerFunc = ir.HandlerFunc
// A Middleware wraps a command's handler in another handler, which runs
// first and decides whether to call the one it wrapped. Declare one with
// Command.Middleware.
//
// func timing(next climux.HandlerFunc) climux.HandlerFunc {
// return func(ctx context.Context, inv *climux.Invocation) error {
// start := time.Now()
// err := next(ctx, inv)
// fmt.Fprintf(inv.Stderr, "%s took %s\n", inv.Cmd.FullName, time.Since(start))
// return err
// }
// }
//
// Do the work in the handler returned, not in the wrapper itself: the
// wrapper may be called more than once per run. An interrupt, such as
// --help, runs no wrapper: it runs in place of the handler rather than
// as one.
type Middleware func(HandlerFunc) HandlerFunc
// Command configures a command that users may invoke from the command line.
//
// Programs should not create Command directly and instead use NewCommand to
// construct one.
type Command struct {
parent *Command
name string
summary string
description string
hidden bool
forwardArgs bool
// interrupt, if set, is what makes the command an interrupt, the
// way a Handler makes a flag one: the callback is the marker, and
// invoking the command runs it in place of any handler, skipping
// the checks an ordinary command line must pass. There is no
// chained setter for it -- a program wanting one of its own reaches
// for InterruptCommand, the same constructor this package's own are
// built with.
interrupt HandlerFunc
// forwardedValueName and forwardedUsage name and explain the
// arguments the command forwards unparsed; see Command.Forwarded.
forwardedValueName string
forwardedUsage string
flagGroups []*FlagGroup
groupSets []*GroupSet
subcommands []*Command
usageFunc ir.UsageFunc
handlerFunc HandlerFunc
middleware []Middleware
// completionEnabled records that EnableCompletion was called on this
// command, so Run consults the shell completion environment variable
// before doing anything else. See EnableCompletion.
completionEnabled bool
// defaultGroup is the implicit "options" flag group that Flags appends
// to. NewCommand creates it eagerly, so every command carries one from
// construction; it stays out of help output regardless, since Usage
// skips a group with no flags in it.
defaultGroup *FlagGroup
}
// NewCommand returns a new Command with the given name and summary.
//
// summary is the one-line description of the command, shown beside its name
// where a parent lists its subcommands, and beneath the usage line in the
// command's own help message. See Command.Description for the longer prose
// that follows it.
func NewCommand(name, summary string) *Command {
return (&Command{
name: name,
summary: summary,
}).Flags()
}
// InterruptCommand returns a Command that ends the program with fn
// before any handlers or middleware run, the way --help and --version
// do, even when the rest of the command line is incomplete or wrong.
//
// Everything after the command's name reaches fn unparsed, as
// Invocation.Forwarded; an interrupt declares no flags and no
// subcommands of its own. VersionCommand and SchemaCommand are two of
// these, ready made.
func InterruptCommand(name, summary string, fn HandlerFunc) *Command {
c := NewCommand(name, summary)
c.interrupt = fn
return c
}
// VersionCommand returns a Command named "version" that prints version,
// alongside the name of the program it is mounted in.
//
// Like HelpFlag, it ends the program before any handlers run. See
// VersionFlag for the same thing spelled as a flag.
//
// Mount it like any other subcommand. Command.VersionCommand is the
// shorthand, and this is the way to mount it somewhere that shorthand
// cannot -- under a subcommand rather than the root, or renamed.
func VersionCommand(version string) *Command {
return InterruptCommand("version", "Show the version", printVersion(version))
}
// SchemaCommand returns a Command named "schema" that writes a JSON
// description of the program it is mounted in to standard output. The
// name is fixed by convention, so tooling can find it without being
// told.
//
// Like VersionCommand, it ends the program before any handlers run.
//
// Mount it like any other subcommand. Command.SchemaCommand is the
// shorthand, and this is the way to mount it somewhere that shorthand
// cannot -- under a subcommand rather than the root, or renamed.
func SchemaCommand() *Command {
return InterruptCommand("schema", "Describe this program as JSON",
func(ctx context.Context, inv *Invocation) error {
enc := json.NewEncoder(inv.Stdout)
enc.SetIndent("", " ")
return enc.Encode(desc.NewDocument(inv.Cmd.Root.Describe()))
})
}
func (c *Command) String() string { return c.name }
// root returns the root of the command tree c belongs to, which is c itself
// if it has no parent, and reports a configuration error if the parent
// links form a cycle instead of reaching a root.
//
// Nothing else walks the source tree: a cycle is representable until
// Compile has rejected it, so every other reader takes its ancestry from
// the compiled tree. See Compile.
func (c *Command) root() (*Command, error) {
seen := map[*Command]struct{}{c: {}}
root := c
for root.parent != nil {
root = root.parent
if _, ok := seen[root]; ok {
return nil, ir.NewConfigErrorf(nil, nil, nil,
"%q is its own ancestor", root.name)
}
seen[root] = struct{}{}
}
return root, nil
}
// validate compiles the whole command tree c belongs to and reports only
// whether it is valid, discarding the compiled tree. See Compile.
func (c *Command) validate() error {
_, err := c.Compile()
return err
}
// effectiveGroups returns the flag groups the command presents: its own,
// followed by every group of every mounted GroupSet, in registration
// order. Validation, parsing and Compile all read flags through this
// helper, so a mounted flag behaves exactly like a declared one. The
// result is assembled afresh on each call, never written back into
// flagGroups: that is what keeps Compile pure and a repeated Parse from
// mounting the same groups twice.
func (c *Command) effectiveGroups() []*FlagGroup {
if len(c.groupSets) == 0 {
return c.flagGroups
}
groups := make([]*FlagGroup, len(c.flagGroups), len(c.flagGroups)+len(c.groupSets))
copy(groups, c.flagGroups)
for _, set := range c.groupSets {
groups = append(groups, set.groups...)
}
return groups
}
// Compile validates the whole command tree c belongs to and returns c in
// its compiled ir.Command form, with its ancestry, inherited flags and
// resolved streams filled in.
//
// Reach for it to check a tree for configuration errors before running it,
// or to walk or marshal a program's whole command line surface. Errors
// anywhere in the tree are reported, not only those in c.
//
// It mutates nothing, so it is safe to call at any time.
func (c *Command) Compile() (*ir.Command, error) {
root, err := c.root()
if err != nil {
// The tree has no root to lower from, so this error stands alone
// rather than joining the batch the rest of the run collects.
return nil, err
}
nodeMap := make(map[*Command]*ir.Command)
var errs []error
rootNode := root.lower(nil, nil, nodeMap, &errs)
if err := rootNode.Validate(); err != nil {
errs = append(errs, err)
}
// Whether two flags collide is a question about the options they are
// written as rather than about the model, so it is argv's to answer;
// see internal/argv.Validate. What a name may be in the first place
// is argv's too, and was settled during lowering, while the names
// were still undecorated.
if err := argv.Validate(rootNode); err != nil {
errs = append(errs, err)
}
if err := ir.JoinErrors(errs); err != nil {
return nil, err
}
return nodeMap[c], nil
}
// lower builds the compiled ir.Command for c and, recursively, each of its
// subcommands, copying across every
// field the ir type keeps, including those inherited from an ancestor so
// nothing about the compiled tree has to walk itself again: FullName,
// computed from parent's own FullName plus c's name, and the three
// streams and the usage renderer, each c's own where it named one and
// the parent's otherwise, and Ancestry, the parent's with c appended.
// Inheritance reads the node being built rather than the source tree's
// parent links, which are not to be trusted until Compile has checked
// them.
//
// Handler is assembled rather than copied: c's own handler is wrapped in
// the middleware c declared and in everything it inherited, which arrives
// as inherited because it is scaffolding for building Handler rather than
// anything a compiled command carries. A command that declared no handler
// gets missingSubcommand's instead, so Handler is never nil and nothing
// downstream needs to know either mechanism exists. An interrupt wraps in
// neither: see where middleware is applied below.
//
// It records the node for c in nodeMap so Compile can look up the node
// for any source *Command after lowering from the root.
//
// It also collects into errs the three configuration checks that cannot
// move onto the compiled tree, because they depend on the source tree's
// own bookkeeping rather than on anything a lowered node carries:
//
// Whether any middleware c declared is nil, which cannot be applied to a
// handler and which chainMiddleware therefore skips. Composing leaves
// nothing on the node to check afterwards, so it is checked here, where
// it is still a list of what one command declared.
//
// Whether a subcommand's parent actually names the command about to
// claim it as a child. See Subcommands for why -- a shared command such
// as climux.CommandLine may be mounted under more than one parent, and
// only the source *Command remembers which one Subcommands actually
// accepted.
//
// Whether a subcommand has already been lowered, which means the
// subcommand links lead back into the tree above. nodeMap is the record
// of what has been visited, so the descent stops there rather than
// building nodes forever. Note that a cycle here need not be one in the
// parent links Compile checked: Subcommands leaves an owned command's
// parent alone, so a command can be mounted below its own ancestor
// without any parent link changing.
func (c *Command) lower(parent *ir.Command, inherited Middleware, nodeMap map[*Command]*ir.Command, errs *[]error) *ir.Command {
fullName := c.name
if parent != nil {
fullName = parent.FullName + " " + c.name
}
node := &ir.Command{
Name: c.name,
Summary: c.summary,
Description: c.description,
Hidden: c.hidden,
ForwardArgs: c.forwardArgs,
Interrupt: c.interrupt,
ForwardedValueName: argv.ForwardedValueNameFor(c.forwardedValueName),
ForwardedUsage: c.forwardedUsage,
FullName: fullName,
Handler: c.handlerFunc,
UsageFunc: c.usageFunc,
}
node.Root = node
node.Ancestry = []*ir.Command{node}
if parent != nil {
node.Root = parent.Root
// Cloned rather than appended in place, or sibling subcommands
// would share a backing array and overwrite each other.
node.Ancestry = append(slices.Clone(parent.Ancestry), node)
}
// What c named for itself is above; what it left unnamed is inherited
// here. Each field resolves on its own, so naming one leaves the
// others inherited, and a parent's are already resolved, so
// inheriting ends the search.
if parent != nil {
if node.UsageFunc == nil {
node.UsageFunc = parent.UsageFunc
}
}
// Middleware composes where the fields above fall back: a command's
// own wrappers run inside every one its ancestors declared, so what
// this command adds is wrapped by what it inherited rather than
// replacing it.
for _, mw := range c.middleware {
if mw == nil {
*errs = append(*errs, ir.NewConfigErrorf(nil, node, nil,
"middleware must not be nil"))
}
}
middleware := chainMiddleware(inherited, c.middleware)
// The handler is assembled here rather than at dispatch, so the
// compiled command carries the whole of what it does and nothing
// downstream has to know that middleware exists. The fallback is not
// wrapped: there is no handler for a wrapper to wrap, and a command
// that only groups subcommands must not run its ancestors' wrappers.
// An interrupt is not wrapped either, own or inherited, the way an
// interrupt flag's Handler never is: middleware is written against a
// command line that parsed, and an interrupt answers one that may
// not have.
if node.Handler == nil {
node.Handler = missingSubcommand(node)
} else if middleware != nil && c.interrupt == nil {
node.Handler = middleware(node.Handler)
}
if c.interrupt != nil && c.handlerFunc != nil {
*errs = append(*errs, ir.NewConfigErrorf(nil, node, nil,
"a command is an interrupt or has a handler, not both"))
}
// The root has no parent to inherit from, so a stream nobody named is
// the process's. UsageFunc has no default to fall back to here: nil
// means the help renderer chooses one when it prints.
nodeMap[c] = node
// Own groups first, then every mounted set, matching the order
// effectiveGroups reports and marking which is which -- validation
// needs to tell a declared flag from a mounted one.
for _, group := range c.flagGroups {
node.FlagGroups = append(node.FlagGroups, group.lower(false, errs))
}
for _, set := range c.groupSets {
for _, group := range set.groups {
node.FlagGroups = append(node.FlagGroups, group.lower(true, errs))
}
}
for _, sub := range c.subcommands {
if prev, ok := nodeMap[sub]; ok {
*errs = append(*errs, ir.NewConfigErrorf(nil, node, nil,
"%q is already mounted at %q", sub.name, prev.FullName))
continue
}
// Subcommands leaves an already-parented command's parent alone
// rather than stealing it, so the mismatch is still visible here
// to report.
if sub.parent != c {
*errs = append(*errs, ir.NewConfigErrorf(nil, node, nil,
"%q is already a subcommand of %q", sub.name, sub.parent.name))
}
node.Subcommands = append(node.Subcommands, sub.lower(node, middleware, nodeMap, errs))
}
return node
}
// chainMiddleware composes outer, the middleware a command inherits from
// its parent, with own, the middleware that command declared, into the
// single Middleware the compiled command carries.
//
// Applying the result wraps a handler so that outer runs first, then
// own's entries in the order they were declared, then the handler itself,
// each resuming in reverse as the call returns. The outermost wrapper is
// therefore the one declared highest in the tree, and earliest in its
// command's Middleware call.
//
// A command that adds nothing inherits outer unchanged, so a path that
// declared no middleware at all composes to nil and costs no call.
//
// A nil entry in own is skipped rather than applied. Lowering reports one
// as a configuration error and then has to finish, so that the rest of
// the tree's errors are collected in the same run; the tree it composes
// meanwhile is never dispatched.
func chainMiddleware(outer Middleware, own []Middleware) Middleware {
if len(own) == 0 {
return outer
}
return func(next HandlerFunc) HandlerFunc {
// Built inside out, which reads backwards from the order above:
// the wrapper applied last ends up outermost, and so runs first.
for _, mw := range slices.Backward(own) {
if mw == nil {
continue
}
next = mw(next)
}
if outer != nil {
next = outer(next)
}
return next
}
}
// missingSubcommand returns the handler lowering gives a command that
// declared none of its own. Such a command exists only to group its
// subcommands, so naming it alone is a usage error rather than a request
// for help, and reporting that is the whole of what it does.
//
// It names cmd rather than the invocation's command so that the error
// names the command that lacked the handler, whoever ran it.
func missingSubcommand(cmd *ir.Command) HandlerFunc {
return func(ctx context.Context, inv *Invocation) error {
return ir.NewArgumentErrorf(nil, cmd, nil, "", "missing subcommand")
}
}
// Description specifies the prose printed at the end of this command's help
// message, after its flags and subcommands. It carries the detail that does
// not fit the one-line summary given to NewCommand.
func (c *Command) Description(s string) *Command {
c.description = s
return c
}
// HandleFunc registers the handler for the command. If no handler is
// specified and the command is invoked, Run reports an argument error
// followed by the command's usage and exits with the usage error code.
//
// The handler runs inside whatever middleware the command and its
// ancestors declared, Compile having wrapped it while lowering the tree;
// see Command.Middleware.
func (c *Command) HandleFunc(handler HandlerFunc) *Command {
c.handlerFunc = handler
return c
}
// Middleware wraps the handler of this command, and of every command
// beneath it, in each of the given wrappers -- an authorization check, a
// timing trace, a resource opened and closed -- written once instead of at
// the top of every handler.
//
// var App = climux.NewCommand("myapp", "Do things").
// Middleware(authorize, trace).
// Subcommands(GetCommand, DeleteCommand)
//
// The outermost wrapper is the one declared highest in the tree, and
// within one command they run in the order given here. Repeated calls
// append.
//
// A wrapper runs only around a handler, and only once the command line has
// parsed, so it may read the flags the user set. Returning an error
// without calling the handler refuses the invocation. Neither an interrupt
// such as --help nor a command that only groups subcommands runs a handler,
// so neither runs a wrapper. See Middleware.
func (c *Command) Middleware(mw ...Middleware) *Command {
c.middleware = append(c.middleware, mw...)
return c
}
// Hidden hides the command from all help messages but still allows the command
// to be invoked on the command line.
func (c *Command) Hidden() *Command {
c.hidden = true
return c
}
// Flags appends command line flags to the implicit "options" flag group
// every command carries from construction.
func (c *Command) Flags(flags ...*Flag) *Command {
if c.defaultGroup == nil {
c.defaultGroup = &FlagGroup{name: "options", title: "Options"}
c.flagGroups = append(c.flagGroups, c.defaultGroup)
}
c.defaultGroup.flags = append(c.defaultGroup.flags, flags...)
return c
}
// HelpFlag adds the flag that prints this command's help message. Given
// no names it answers to "--help" and "-h".
//
// var App = climux.NewCommand("myapp", "My application").HelpFlag()
//
// Call it on the root: every command below answers to the flag too, and
// each prints its own help. It is shorthand for adding HelpFlag with
// Flags, which is the way to hide it, or to name it something else
// entirely.
func (c *Command) HelpFlag(names ...string) *Command {
return c.Flags(HelpFlag(names...))
}
// VersionFlag adds the flag that prints version, alongside the name of
// the program it is mounted in. Given no names it answers to "--version".
//
// var App = climux.NewCommand("orbital", "").VersionFlag(version)
//
// It is shorthand for adding VersionFlag with Flags, which is the way
// to put it in a group of its own, or to hide it.
func (c *Command) VersionFlag(version string, names ...string) *Command {
return c.Flags(VersionFlag(version, names...))
}
// VersionCommand adds a subcommand named "version" that prints version,
// alongside the name of the program it is mounted in.
//
// Like HelpFlag, it ends the program before any handlers run. See
// VersionFlag for the same thing spelled as a flag.
func (c *Command) VersionCommand(version string) *Command {
return c.Subcommands(VersionCommand(version))
}
// SchemaCommand adds a subcommand named "schema" that writes a JSON
// description of the program to standard output. Like VersionCommand,
// it ends the program before any handlers run.
func (c *Command) SchemaCommand() *Command {
return c.Subcommands(SchemaCommand())
}
// FlagGroups adds groups of command line flags, built with NewFlagGroup or
// FromFlagSet, to this command, showing each under its own heading in help
// messages.
func (c *Command) FlagGroups(groups ...*FlagGroup) *Command {
c.flagGroups = append(c.flagGroups, groups...)
return c
}
// GroupSets mounts every flag group registered in each of the given sets on
// this command, after the command's own groups. Mount CommandLine to pick
// up everything the program's libraries registered:
//
// var App = climux.NewCommand("myapp", "").GroupSets(climux.CommandLine)
//
// A group registered after this call is still picked up. Mounted flags
// behave exactly like the command's own, each group under its own heading
// in help messages.
func (c *Command) GroupSets(sets ...*GroupSet) *Command {
c.groupSets = append(c.groupSets, sets...)
return c
}
// Subcommands adds subcommands to this command and sets their parent to
// this command, unless a command given here already has one -- typically a
// command already mounted elsewhere, such as climux.CommandLine -- in
// which case its existing parent is left alone and validation reports the
// mismatch; see lower.
func (c *Command) Subcommands(cmds ...*Command) *Command {
c.subcommands = append(c.subcommands, cmds...)
for _, cmd := range cmds {
if cmd.parent == nil {
cmd.parent = c
}
}
return c
}
// UsageFunc specifies a custom renderer for this command's help messages,
// in place of the default.
func (c *Command) UsageFunc(fn ir.UsageFunc) *Command {
c.usageFunc = fn
return c
}
// EnableCompletion opts this command into shell completion. Call it on the
// command Run is given, typically the root, since the environment variable
// it enables is named from that command.
//
// The variable is the command's name, uppercased, with every rune that is
// not a letter or digit replaced by "_", and "_COMPLETE" appended: "myapp"
// answers to MYAPP_COMPLETE. A user enables completion in their shell with
//
// source <(MYAPP_COMPLETE=bash_source myapp 2>/dev/null)
//
// Run is otherwise unchanged, including when the variable is unset.
func (c *Command) EnableCompletion() *Command {
c.completionEnabled = true
return c
}
// ForwardArgs specifies that a "--" on the command line ends option
// processing, and that everything after it reaches the handler unparsed as
// Invocation.Forwarded rather than binding to positional flags.
//
// This is for a command that hands arguments on to something else, such as
// a subprocess. Without it, "--" has no special meaning.
func (c *Command) ForwardArgs() *Command {
c.forwardArgs = true
return c
}
// Forwarded names the arguments the command forwards to its handler
// unparsed, for help and for the machine-readable description: what an
// interrupt command takes after its name, or what a command that set
// ForwardArgs takes after the "--" terminator.
//
// InterruptCommand("schema", summary, fn).
// Forwarded("command", "Command to describe")
//
// valueName is shown where the forwarded arguments are, the way a
// positional argument's name is shown, and usage explains them beneath
// the usage line. Naming forwarded arguments on a command that forwards
// nothing is a configuration error.
func (c *Command) Forwarded(valueName, usage string) *Command {
c.forwardedValueName = valueName
c.forwardedUsage = usage
return c
}