@@ -59,6 +59,12 @@ type Command struct {
59
59
Middleware MiddlewareFunc
60
60
Handler HandlerFunc
61
61
HelpHandler HandlerFunc
62
+ // CompletionHandler is called when the command is run in completion
63
+ // mode. If nil, only the default completion handler is used.
64
+ //
65
+ // Flag and option parsing is best-effort in this mode, so even if an Option
66
+ // is "required" it may not be set.
67
+ CompletionHandler CompletionHandlerFunc
62
68
}
63
69
64
70
// AddSubcommands adds the given subcommands, setting their
@@ -193,15 +199,22 @@ type Invocation struct {
193
199
ctx context.Context
194
200
Command * Command
195
201
parsedFlags * pflag.FlagSet
196
- Args []string
202
+
203
+ // Args is reduced into the remaining arguments after parsing flags
204
+ // during Run.
205
+ Args []string
206
+
197
207
// Environ is a list of environment variables. Use EnvsWithPrefix to parse
198
208
// os.Environ.
199
209
Environ Environ
200
210
Stdout io.Writer
201
211
Stderr io.Writer
202
212
Stdin io.Reader
203
- Logger slog.Logger
204
- Net Net
213
+
214
+ // Deprecated
215
+ Logger slog.Logger
216
+ // Deprecated
217
+ Net Net
205
218
206
219
// testing
207
220
signalNotifyContext func (parent context.Context , signals ... os.Signal ) (ctx context.Context , stop context.CancelFunc )
@@ -282,6 +295,17 @@ func copyFlagSetWithout(fs *pflag.FlagSet, without string) *pflag.FlagSet {
282
295
return fs2
283
296
}
284
297
298
+ func (inv * Invocation ) CurWords () (prev string , cur string ) {
299
+ if len (inv .Args ) == 1 {
300
+ cur = inv .Args [0 ]
301
+ prev = ""
302
+ } else {
303
+ cur = inv .Args [len (inv .Args )- 1 ]
304
+ prev = inv .Args [len (inv .Args )- 2 ]
305
+ }
306
+ return
307
+ }
308
+
285
309
// run recursively executes the command and its children.
286
310
// allArgs is wired through the stack so that global flags can be accepted
287
311
// anywhere in the command invocation.
@@ -378,8 +402,19 @@ func (inv *Invocation) run(state *runState) error {
378
402
}
379
403
}
380
404
405
+ // Outputted completions are not filtered based on the word under the cursor, as every shell we support does this already.
406
+ // We only look at the current word to figure out handler to run, or what directory to inspect.
407
+ if inv .IsCompletionMode () {
408
+ for _ , e := range inv .complete () {
409
+ fmt .Fprintln (inv .Stdout , e )
410
+ }
411
+ return nil
412
+ }
413
+
414
+ ignoreFlagParseErrors := inv .Command .RawArgs
415
+
381
416
// Flag parse errors are irrelevant for raw args commands.
382
- if ! inv . Command . RawArgs && state .flagParseErr != nil && ! errors .Is (state .flagParseErr , pflag .ErrHelp ) {
417
+ if ! ignoreFlagParseErrors && state .flagParseErr != nil && ! errors .Is (state .flagParseErr , pflag .ErrHelp ) {
383
418
return xerrors .Errorf (
384
419
"parsing flags (%v) for %q: %w" ,
385
420
state .allArgs ,
@@ -401,7 +436,7 @@ func (inv *Invocation) run(state *runState) error {
401
436
}
402
437
}
403
438
// Don't error for missing flags if `--help` was supplied.
404
- if len (missing ) > 0 && ! errors .Is (state .flagParseErr , pflag .ErrHelp ) {
439
+ if len (missing ) > 0 && ! inv . IsCompletionMode () && ! errors .Is (state .flagParseErr , pflag .ErrHelp ) {
405
440
return xerrors .Errorf ("Missing values for the required flags: %s" , strings .Join (missing , ", " ))
406
441
}
407
442
@@ -558,6 +593,65 @@ func (inv *Invocation) with(fn func(*Invocation)) *Invocation {
558
593
return & i2
559
594
}
560
595
596
+ func (inv * Invocation ) complete () []string {
597
+ prev , cur := inv .CurWords ()
598
+
599
+ // If the current word is a flag
600
+ if strings .HasPrefix (cur , "--" ) {
601
+ flagParts := strings .Split (cur , "=" )
602
+ flagName := flagParts [0 ][2 :]
603
+ // If it's an equals flag
604
+ if len (flagParts ) == 2 {
605
+ if out := inv .completeFlag (flagName ); out != nil {
606
+ for i , o := range out {
607
+ out [i ] = fmt .Sprintf ("--%s=%s" , flagName , o )
608
+ }
609
+ return out
610
+ }
611
+ } else if out := inv .Command .Options .ByFlag (flagName ); out != nil {
612
+ // If the current word is a valid flag, auto-complete it so the
613
+ // shell moves the cursor
614
+ return []string {cur }
615
+ }
616
+ }
617
+ // If the previous word is a flag, then we're writing it's value
618
+ // and we should check it's handler
619
+ if strings .HasPrefix (prev , "--" ) {
620
+ word := prev [2 :]
621
+ if out := inv .completeFlag (word ); out != nil {
622
+ return out
623
+ }
624
+ }
625
+ // If the current word is the command, move the shell cursor
626
+ if inv .Command .Name () == cur {
627
+ return []string {inv .Command .Name ()}
628
+ }
629
+ var completions []string
630
+
631
+ if inv .Command .CompletionHandler != nil {
632
+ completions = append (completions , inv .Command .CompletionHandler (inv )... )
633
+ }
634
+
635
+ completions = append (completions , DefaultCompletionHandler (inv )... )
636
+
637
+ return completions
638
+ }
639
+
640
+ func (inv * Invocation ) completeFlag (word string ) []string {
641
+ opt := inv .Command .Options .ByFlag (word )
642
+ if opt == nil {
643
+ return nil
644
+ }
645
+ if opt .CompletionHandler != nil {
646
+ return opt .CompletionHandler (inv )
647
+ }
648
+ val , ok := opt .Value .(* Enum )
649
+ if ok {
650
+ return val .Choices
651
+ }
652
+ return nil
653
+ }
654
+
561
655
// MiddlewareFunc returns the next handler in the chain,
562
656
// or nil if there are no more.
563
657
type MiddlewareFunc func (next HandlerFunc ) HandlerFunc
@@ -642,3 +736,5 @@ func RequireRangeArgs(start, end int) MiddlewareFunc {
642
736
643
737
// HandlerFunc handles an Invocation of a command.
644
738
type HandlerFunc func (i * Invocation ) error
739
+
740
+ type CompletionHandlerFunc func (i * Invocation ) []string
0 commit comments