feat(completion): add bash and zsh shell completion

Wrappers in completions/ call --generate-shell-completion; a Go
ShellComplete callback emits flag names so urfave's default response
(subcommands only) is replaced with the full candidate set.

This replaces old hard-coded way to auto-complete using shell script's
parsing.

Signed-off-by: Luca Di Maio <luca.dimaio1@gmail.com>
This commit is contained in:
Luca Di Maio
2026-06-25 13:39:47 +02:00
committed by Alessio Biancalana
parent c71075124b
commit 4acf784a7d
5 changed files with 165 additions and 441 deletions
+55
View File
@@ -0,0 +1,55 @@
package cli
import (
"context"
"fmt"
"github.com/urfave/cli/v3"
)
// flagCompleter prints the command's subcommand names (with aliases) and
// long-form flag names to stdout, one per line. urfave/cli v3's default
// shell completion only emits subcommand names; this fills the gap so
// `distrobox <cmd> --<TAB>` completes flags too.
//
// The bash/zsh wrapper scripts filter candidates by the user's current
// prefix, so emitting the full set unconditionally is the simplest correct
// shape — bash compgen drops anything that doesn't start with `--` when the
// user typed `--`, and drops anything that doesn't match the subcommand
// prefix otherwise.
func flagCompleter(_ context.Context, c *cli.Command) {
// VisibleCommands skips help and any Hidden subcommands, matching
// what the user would see in `--help` output.
for _, sub := range c.VisibleCommands() {
fmt.Println(sub.Name)
for _, alias := range sub.Aliases {
fmt.Println(alias)
}
}
// VisibleFlags skips Hidden flags (e.g. --container-manager,
// --sudo-command, --generate-shell-completion).
seen := make(map[string]struct{})
for _, f := range c.VisibleFlags() {
for _, name := range f.Names() {
if len(name) <= 1 {
continue
}
if _, dup := seen[name]; dup {
continue
}
seen[name] = struct{}{}
fmt.Println("--" + name)
}
}
}
// installShellCompleteRecursively walks a command tree and installs
// flagCompleter on every node, so nested subcommands (e.g.
// `distrobox assemble create`) get the same completion behaviour as
// the top-level subcommands.
func installShellCompleteRecursively(cmd *cli.Command) {
cmd.ShellComplete = flagCompleter
for _, sub := range cmd.Commands {
installShellCompleteRecursively(sub)
}
}
+14 -4
View File
@@ -22,10 +22,20 @@ type contextKey string
const containerManagerKey contextKey = "containerManager"
func NewRootCommand(cfg *config.Values) *cli.Command {
subs := subcommands(cfg)
// Install flag-aware completion on every command, including nested
// `assemble create`/`assemble rm`. The default would only list
// subcommand names; this also emits flag names.
for _, sub := range subs {
installShellCompleteRecursively(sub)
}
return &cli.Command{
Name: "distrobox",
Usage: "Use any Linux distribution inside your terminal",
Version: version.Version,
Name: "distrobox",
Usage: "Use any Linux distribution inside your terminal",
Version: version.Version,
EnableShellCompletion: true,
ShellComplete: flagCompleter,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "verbose",
@@ -40,7 +50,7 @@ func NewRootCommand(cfg *config.Values) *cli.Command {
Value: cfg.SudoProgram,
},
},
Commands: subcommands(cfg),
Commands: subs,
}
}