Skip to content

Shell completion

duho generates static completion scripts for bash, zsh, fish, and PowerShell. Static means the script is a plain shell function you install once — there's no runtime dependency and, unlike argcomplete, your program is not re-invoked on every keypress.

Adding the flag

Set _completion_ = True to add a --print-completion {bash,zsh,fish,powershell} option:

import duho

class App(duho.Args):
    """My tool."""

    _completion_ = True
    _subcommands_ = [Serve, Build]
# bash
app --print-completion bash > ~/.local/share/bash-completion/completions/app

# zsh (somewhere on your $fpath)
app --print-completion zsh > ~/.zfunc/_app

# fish
app --print-completion fish > ~/.config/fish/completions/app.fish
# PowerShell — emit and evaluate (add to your $PROFILE to persist)
app --print-completion powershell | Out-String | Invoke-Expression

The PowerShell emitter registers a Register-ArgumentCompleter -Native script block that resolves the current subcommand path to its flags, subcommand names, and choice values; a command taking a free/path value offers nothing, so PowerShell's own file completion takes over.

_completion_ is off by default — the same opt-in precedent as _version_ — so the flag doesn't clutter --help for tools that don't want it.

As a subcommand

Set _completion_command_ on a Cli root to register a subcommand that prints the script instead (True names it completion, a string names it itself):

import duho
from duho import Cli, Cmd
from duho.testing import invoke

class Leaf(Cmd):
    def __call__(self):
        return 0

class App(Cli):
    _parsername_ = "myapp"
    _completion_command_ = True
    _subcommands_ = [Leaf]

result = invoke(App, ["completion", "fish"])
assert result.status == 0 and result.stdout.startswith("# fish completion for myapp")
myapp completion bash > ~/.local/share/bash-completion/completions/myapp

It takes one required positional, the shell (bash, zsh, fish or powershell), and writes that shell's script to stdout; the script includes the completion subcommand itself. The name must not contain whitespace or start with -, the app needs at least one other subcommand, and the name must be free (also against the MCP subcommand's name): otherwise the build raises ValueError naming _completion_command_. duho.main and duho.app both read it; there is no keyword argument. The command is never an MCP tool, and --print-completion is unchanged.

Without the flag

Generate a script without exposing the option at all:

import sys
import duho

duho.print_completion(App, "bash", file=sys.stdout, prog="app")

prog= is the command name the script binds to. Without it the script binds the application's name — the same name whether it is generated by the app itself or by a separate script. Pass prog= when the command users type differs from that name.

What gets completed

The generator walks the built parser tree, so it knows everything duho knows:

  • Subcommands, including nested _subcommands_ trees.
  • Option flags for the command and for whichever subcommand is being typed.
  • Choices — a Literal or enum.Enum field offers its values as completion candidates.
  • Paths — a pathlib.Path-typed field gets the shell's native file and directory completion.

Known limitations

  • zsh and fish tokenize a completion candidate through their own word/action syntax, which has no way to carry a literal newline: a choice value containing \n loses it there (the newline is dropped, not escaped to a visible placeholder). bash and PowerShell do not have this limitation.

Regenerating

The script is a snapshot of your CLI's shape. Regenerate it when you add or rename commands or options — typically as a release step, or from a make target, so the shipped completions never drift from the actual interface.