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
Literalorenum.Enumfield 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
\nloses 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.