@@ -59,6 +59,12 @@ type Command struct {
5959 Middleware MiddlewareFunc
6060 Handler HandlerFunc
6161 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
6268}
6369
6470// AddSubcommands adds the given subcommands, setting their
@@ -193,15 +199,22 @@ type Invocation struct {
193199 ctx context.Context
194200 Command * Command
195201 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+
197207 // Environ is a list of environment variables. Use EnvsWithPrefix to parse
198208 // os.Environ.
199209 Environ Environ
200210 Stdout io.Writer
201211 Stderr io.Writer
202212 Stdin io.Reader
203- Logger slog.Logger
204- Net Net
213+
214+ // Deprecated
215+ Logger slog.Logger
216+ // Deprecated
217+ Net Net
205218
206219 // testing
207220 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 {
282295 return fs2
283296}
284297
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+
285309// run recursively executes the command and its children.
286310// allArgs is wired through the stack so that global flags can be accepted
287311// anywhere in the command invocation.
@@ -378,8 +402,19 @@ func (inv *Invocation) run(state *runState) error {
378402 }
379403 }
380404
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+
381416 // 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 ) {
383418 return xerrors .Errorf (
384419 "parsing flags (%v) for %q: %w" ,
385420 state .allArgs ,
@@ -401,7 +436,7 @@ func (inv *Invocation) run(state *runState) error {
401436 }
402437 }
403438 // 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 ) {
405440 return xerrors .Errorf ("Missing values for the required flags: %s" , strings .Join (missing , ", " ))
406441 }
407442
@@ -558,6 +593,65 @@ func (inv *Invocation) with(fn func(*Invocation)) *Invocation {
558593 return & i2
559594}
560595
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+
561655// MiddlewareFunc returns the next handler in the chain,
562656// or nil if there are no more.
563657type MiddlewareFunc func (next HandlerFunc ) HandlerFunc
@@ -642,3 +736,5 @@ func RequireRangeArgs(start, end int) MiddlewareFunc {
642736
643737// HandlerFunc handles an Invocation of a command.
644738type HandlerFunc func (i * Invocation ) error
739+
740+ type CompletionHandlerFunc func (i * Invocation ) []string
0 commit comments