Shell Completion
goopt generates runtime shell completion for Bash, Zsh, Fish and PowerShell.
Instead of baking your command tree into a large static script, goopt installs a tiny
stub that forwards each <TAB> back to your program. The live parser then computes
the suggestions. This has two consequences that matter:
- It can’t drift. Completion is computed by the same parser that parses your command line, so it offers exactly the flags and commands the parser actually accepts — including inherited flags on subcommands. There is no second model to keep in sync.
- It can be dynamic. A flag’s values can be computed at completion time (git
branches, files in context, rows from a service) via
WithCompleter— something a static script can never do.
Migrating from the old static generator (
GenerateCompletion/GetCompletionData)? See Migrating from static completion below.
How it works
- You install a small stub for the user’s shell (generated by
GenerateCompletionStub). - On
<TAB>, the stub runsyourapp __complete <shell> <words...>. - Your
maindetects that hidden request before parsing and lets goopt answer it:parser.HandleCompletion(os.Args, os.Stdout).
goopt never owns your main or runs your command logic during completion — it resolves
structure only (no callbacks, no binding, no validation, no output) and you own the exit.
Quick start
Two pieces: a responder in main, and a completion command that installs the stub.
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/napalu/goopt/v2"
comp "github.com/napalu/goopt/v2/completion"
)
func main() {
parser := buildParser()
// 1) Answer completion requests before parsing. If this was a `__complete`
// invocation, goopt has written the suggestions — we just exit.
if parser.HandleCompletion(os.Args, os.Stdout) {
return
}
if !parser.Parse(os.Args) {
// ... handle parse errors ...
os.Exit(1)
}
// ... run your program ...
}
func buildParser() *goopt.Parser {
parser := goopt.NewParser()
// ... your flags and commands ...
// 2) A `completion` command that installs the stub for the user's shell.
parser.AddCommand(goopt.NewCommand(
goopt.WithName("completion"),
goopt.WithCommandDescription("Install shell completion"),
goopt.WithCallback(func(p *goopt.Parser, _ *goopt.Command) error {
shell := "bash" // in a real app, take this from an argument
exec, err := os.Executable()
if err != nil {
return err
}
stub, err := p.GenerateCompletionStub(shell, filepath.Base(exec))
if err != nil {
return err
}
manager, err := comp.NewManager(shell, exec)
if err != nil {
return err
}
manager.Accept(stub) // install the stub
path, err := manager.Save()
if err != nil {
return err
}
fmt.Printf("%s completion installed at %s\n", shell, path)
return nil
}),
))
return parser
}
Prefer to print the stub and let the user redirect it? Skip the Manager and just
fmt.Print(stub) — e.g. source <(yourapp completion bash).
Dynamic value completion
WithCompleter supplies a flag’s candidate values at completion time. It is never
called during normal parsing.
goopt.NewArg(
goopt.WithCompleter(func(c goopt.CompleterContext) []goopt.Suggestion {
// c.Prefix — the partial value typed so far
// c.Command — the resolved command path at the cursor
// c.Parser — the parser, for context-dependent completion
return gitBranches(c.Prefix)
}),
)
A Suggestion is {Value, Description}; the description is shown by shells that support
it (zsh, fish, PowerShell) and ignored by bash. goopt prefix-filters the returned values
by default.
For a fixed set, a validator that knows its values drives completion for free.
validation.IsOneOf implements validation.Enumerable, so one declaration both
validates input against the set and completes it — the single-source successor
to the deprecated WithAcceptedValues:
goopt.NewArg(goopt.WithValidators(validation.IsOneOf("prod", "staging", "dev")))
// rejects anything outside the set AND offers the three values on <TAB>
Any validator implementing Enumerable (Candidates() []string) feeds completion the
same way — validation and completion can’t drift because they’re the same value.
(WithCompleter returning a constant slice works too, but only completes — it doesn’t
validate.)
A File-type flag automatically gets shell path completion — no completer needed.
Installation (end users)
The install command is the same regardless of shell; only the target file differs (the
Manager knows each shell’s convention). The installed file is a small stub, so your
binary must be on PATH when completion runs — which it already is for any CLI you’re
completing.
yourapp completion bash # installs (or prints) the stub
Notes & limitations
- Reserved hidden command. Completion is driven by a hidden
__completesubcommand thatHandleCompletionintercepts before parsing. Don’t define a real command named__complete— it would be shadowed by the completion responder. - Cursor position. Completion targets the word under the cursor. Bash, Zsh and Fish
report the cursor word (
COMP_CWORD/$CURRENT/commandline -ct), so completing in the middle of a line works. PowerShell completes against the final token. - Binary on
PATH. Because the installed file is a stub, the program must be resolvable when completion runs (always true for a CLI you’re completing).
Migrating from static completion
The previous static generator (GenerateCompletion, GetCompletionData, and the
completion package generators) has been removed in favour of the runtime system.
The migration is small:
| Before (static) | After (runtime) |
|---|---|
| — | Add if parser.HandleCompletion(os.Args, os.Stdout) { return } before Parse |
manager.Accept(p.GetCompletionData()) |
stub, _ := p.GenerateCompletionStub(shell, name) then manager.Accept(stub) |
p.GenerateCompletion(shell, name) |
p.GenerateCompletionStub(shell, name) |
WithAcceptedValues(...) for completion |
WithCompleter(func(c) []Suggestion { ... }) |
Manager.Save() and its shell-path logic are unchanged — only Accept now takes the
stub string instead of CompletionData. The end-user install command is unchanged.
Automated migration tool
The same release made validation.Validator an interface (so validators can drive
completion). A bare inline func(string) error passed to WithValidator(s) must now be
wrapped in validation.Custom(...). The goopt-migrate-v2 codegen tool does
this (and renames []validation.ValidatorFunc → []validation.Validator) automatically:
go install github.com/napalu/goopt/v2/cmd/goopt-migrate-v2@latest
goopt-migrate-v2 -d . -r --dry-run # preview; drop --dry-run to apply
The binary is named goopt-migrate-v2 so it does not clash with the v1→v2 tag tool
(goopt-migrate). It rewrites what it can prove syntactically; the compiler flags the
rest (e.g. a func-typed variable) for a one-line manual wrap. See the v2/migration
package README for details and limits.
Why the break (and why now): the static model was a second representation of your command tree that could disagree with the parser (inherited flags missing from subcommands, value completion silently dropped). The runtime system computes from the one true source, so that class of bug cannot occur — and it adds dynamic value completion the static model could never express.