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

  1. You install a small stub for the user’s shell (generated by GenerateCompletionStub).
  2. On <TAB>, the stub runs yourapp __complete <shell> <words...>.
  3. Your main detects 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 __complete subcommand that HandleCompletion intercepts 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.


This site uses Just the Docs, a documentation theme for Jekyll.