Carapace completion¶
Generating a spec¶
Carapace is a multi-shell completion engine: one spec file drives identical completions across Bash, Zsh, Fish, Nushell, PowerShell, Elvish, Xonsh and more. click_extra.carapace walks a Click command tree and serializes it to the carapace-spec YAML format, so a CLI gets completion everywhere Carapace runs. This is Click Extra’s answer to click#3188, the request for native Carapace support that fell outside the scope of core Click.
The spec is produced mechanically from the command itself, on any Click command object (no console_scripts entry point required). Take a small CLI:
import click
@click.group()
def weather():
"""Show the weather."""
@weather.command()
@click.option("--unit", type=click.Choice(["celsius", "fahrenheit"]), help="Scale.")
@click.option("--report", type=click.Path(exists=True), help="Read a saved report.")
@click.argument("city")
def forecast(unit, report, city):
"""Forecast for a CITY."""
dump_carapace_spec renders its spec. Choices are inlined, a path operand becomes the $files action, and the subcommand tree is reproduced verbatim:
$ emit
# Generated by Click Extra 8.1.0.dev0. Do not edit by hand.
# Documentation: https://kdeldycke.github.io/click-extra/carapace.html
name: weather
description: Show the weather.
persistentflags:
--help: Show this message and exit.
commands:
- name: forecast
description: Forecast for a CITY.
flags:
--unit=: Scale.
--report=: Read a saved report.
completion:
flag:
unit:
- celsius
- fahrenheit
report:
- $files
The wrap --carapace mode¶
click-extra wrap --carapace -- SCRIPT resolves a target, loads its Click command, and prints the whole tree’s spec to stdout without running it. SCRIPT is resolved the same way as for --man, so nothing needs to be installed up front with uvx:
$ uvx --from "click-extra[carapace]" --with flask click-extra wrap --carapace -- flask > flask.yaml
--carapace must appear before SCRIPT, since arguments after SCRIPT navigate into nested subcommands. It is mutually exclusive with --man and --show-params.
Pass --install to write the spec straight into Carapace’s user spec directory ($XDG_CONFIG_HOME/carapace/specs/, which Carapace loads on startup) instead of printing it:
$ uvx --from "click-extra[carapace]" --with flask click-extra wrap --carapace --install -- flask
/home/me/.config/carapace/specs/flask.yaml
Commands discovered from external state¶
The spec is a point-in-time snapshot of the command tree. Most groups expose a fixed set of subcommands, but some compute theirs from external state: a loaded application, installed plugins, or a scanned directory. The exporter walks the tree through the group’s own list_commands and get_command, so the spec captures exactly what those return at generation time. Anything the group cannot see at that moment is left out.
A group that registers an extra command only when an optional integration is configured shows the effect. Here that integration is stood in for by the GARDEN_PLOTS environment variable:
import os
import click
@click.command()
def water():
"""Water the garden."""
@click.command()
def harvest():
"""Pick ripe produce."""
class GardenGroup(click.Group):
"""A garden that grows an extra command once its plots are configured."""
def list_commands(self, ctx):
names = ["water"]
if os.environ.get("GARDEN_PLOTS"):
names.append("harvest")
return names
def get_command(self, ctx, name):
return {"water": water, "harvest": harvest}.get(name)
garden = GardenGroup(name="garden", help="Tend a garden.")
With nothing configured, only the built-in water reaches the spec; configuring GARDEN_PLOTS brings harvest in:
$ emit
# Generated by Click Extra 8.1.0.dev0. Do not edit by hand.
# Documentation: https://kdeldycke.github.io/click-extra/carapace.html
name: garden
description: Tend a garden.
persistentflags:
--help: Show this message and exit.
commands:
- name: water
description: Water the garden.
harvest available once configured: True
Flask hits this in practice. Its flask command lists the built-in routes, run and shell, then adds whatever commands the loaded application registered, so it needs to find an application to enumerate the full set. Wrap it with none in reach and the spec carries only the three built-ins, alongside the red Could not locate a Flask application error Flask prints to stderr. That error is Flask’s own and is not fatal: Flask catches it, falls back to the built-ins and carries on, so the YAML on stdout stays valid, only incomplete. Point Flask at an application through the FLASK_APP environment variable (or a wsgi.py or app.py in the working directory) and the error clears and the application’s own commands join the spec:
$ FLASK_APP=myapp uvx --from "click-extra[carapace]" --with flask click-extra wrap --carapace -- flask > flask.yaml
The flask --app option cannot stand in here: the spec is built without running Flask’s own argument parsing, so the application must be discoverable from the environment or the working directory.
Static and dynamic completion¶
Two strategies cooperate, and the generator picks per parameter:
Static. Choices, file and directory operands, and the command hierarchy are frozen into the spec. They complete with no process launch and work in every shell Carapace supports: this is what a spec buys over Carapace’s bridge to a single shell’s native Click completion.
Dynamic. A parameter with a custom
shell_complete(a callback, or aParamTypethat overridesshell_complete) cannot be frozen, so its spec action calls back into the CLI. The callback reuses Click’s own completion machinery through thecarapacecompletion class, registered onimport click_extra.
Dynamic completion therefore needs that class registered in the target process, which a CLI built with Click Extra gets automatically. A plain Click CLI would have to import click_extra for the callback to resolve. Static completion has no such requirement.
A click-extra command also carries its default options (--version, --verbosity, --color, and the rest). On the root command these are emitted as Carapace persistentflags, so every subcommand inherits them without the spec repeating them.
Programmatic API¶
Three entry points cover the Python side, from a string to an installed file:
to_carapace_spec(cli, prog_name=...)returns the spec as a plain dict, ready foryaml.safe_dumpor further processing. It needs no optional dependency.dump_carapace_spec(cli, prog_name=...)serializes that dict to a YAML string with a provenance header.write_carapace_spec(cli, target, prog_name=...)writes the YAML to a path, andinstall_carapace_spec(cli, prog_name=...)writes it to Carapace’s user spec directory and returns the path.
Installation¶
YAML serialization (dump_carapace_spec, write_carapace_spec, install_carapace_spec, and wrap --carapace) needs PyYAML, pulled by the carapace extra:
$ pip install "click-extra[carapace]"
to_carapace_spec and the carapace completion class work without it.
Known limitations¶
Cloup constraints beyond mutual exclusion (
RequireAtLeast,RequireExactly,If) have no carapace-spec equivalent and are dropped: only@option_group(..., constraint=mutually_exclusive)becomesexclusiveflags.The dynamic callback hands the already-typed words to Carapace and lets it filter, so a parameter whose
shell_completedoes its own non-prefix filtering completes more broadly through the spec than it would natively.
click_extra.carapace API¶
classDiagram
ShellComplete <|-- CarapaceComplete
Export a Click command tree as a Carapace completion spec.
Carapace is a multi-shell completion engine: a single spec file drives identical completions across Bash, Zsh, Fish, Nushell, PowerShell, Elvish, Xonsh, Oil and more. This module walks a Click/Cloup command tree and serializes it to the YAML carapace-spec format, answering the request for native Carapace support in Click issue #3188 (closed as out of scope for core Click, redirected here).
Two completion strategies cooperate:
Static. Choices, file/directory operands and the command hierarchy are inlined straight into the spec. These complete with no process launch and work in every shell Carapace supports, which is the advantage a spec has over Carapace’s existing bridge to a single shell’s native Click completion.
Dynamic. A parameter carrying a custom
shell_complete(a callback, or aParamTypethat overridesshell_complete()) cannot be frozen into the spec, so its action calls back into the CLI through Carapace’s shell macro. The callback reuses Click’s own completion machinery viaCarapaceComplete.
Note
Dynamic completion needs the CarapaceComplete class registered in
the target process, which happens on import click_extra. A CLI built with
Click Extra gets it for free; a plain Click CLI would have to import
click_extra for the dynamic callback to resolve. Static completion has no
such requirement.
The dataclasses below mirror the upstream carapace-spec JSON schema; the
flag-key grammar and macro contract are taken from that project’s flag.go and
core.go.
- click_extra.carapace.CARAPACE_SPECS_DIR = PosixPath('/home/runner/.config/carapace/specs')¶
User spec directory Carapace loads on startup.
Writing
<prog>.yamlhere (seeinstall_carapace_spec()) is all it takes for Carapace to pick up a CLI’s completions. Mirrors the$XDG_CONFIG_HOMEdefault documented by Carapace; an explicitXDG_CONFIG_HOMEis honored byinstall_carapace_spec()at call time rather than baked in here.
- click_extra.carapace.CARAPACE_DOCS_URL = 'https://kdeldycke.github.io/click-extra/carapace.html'¶
Documentation page stamped into every generated spec’s header comment, so a reader of the raw YAML knows where the feature is documented.
- class click_extra.carapace.CarapaceCompletion(flag=<factory>, positional=<factory>, positionalany=<factory>)[source]¶
Bases:
objectThe
completionblock of a command: per-flag and positional actions.
- class click_extra.carapace.CarapaceCommand(name, description='', aliases=(), hidden=False, flags=<factory>, persistentflags=<factory>, exclusiveflags=<factory>, completion=<factory>, commands=<factory>)[source]¶
Bases:
objectOne node of a Carapace spec: a command and its flags, completions and subcommands, mirroring the upstream
Commandschema.Whether the command is hidden from listings (still completable).
- persistentflags: dict[str, str]¶
Flags inherited by every subcommand (the root’s default option set).
- completion: CarapaceCompletion¶
Static and dynamic completion actions for this command’s parameters.
- commands: list[CarapaceCommand]¶
Nested subcommands.
- click_extra.carapace.extract_carapace_command(command, ctx, *, is_root, default_opts, inherited_opts, root_name)[source]¶
Build a
CarapaceCommandfrom a Click command and its context.The context must have been created for
command(typically viaclick.Command.make_context()withresilient_parsing=True). Subcommands are discovered dynamically and recursed into.default_optsis the abstract set of spellings Click Extra injects on every command; on the root, options drawn from it becomepersistentflags.inherited_optsis what an ancestor actually published as persistent, so a subcommand drops exactly those (Carapace already offers them) and keeps the rest, including a same-named option the root never carried.root_nameis the binary Carapace dispatches on, used to build dynamic callback macros.- Return type:
- click_extra.carapace.to_carapace_spec(command, prog_name=None, ctx=None)[source]¶
Build the Carapace spec for a command tree as a plain dict.
Reuses
ctxwhen given (the live invocation context), otherwise builds a throwaway one withresilient_parsing=True. The returned mapping conforms to thecarapace-specschema and is ready to hand toyaml.safe_dump.- Return type:
- click_extra.carapace.dump_carapace_spec(command, prog_name=None, ctx=None, *, invocation=None)[source]¶
Serialize a command tree to a Carapace spec YAML string.
Requires the optional PyYAML dependency (
click-extra[carapace]). The output keeps Click’s declaration order rather than sorting keys, so flags and subcommands line up with the help screen, and is prefixed with a provenance header: the generator version, theinvocationcommand line that produced it (when given), and a link to the documentation.- Return type:
- click_extra.carapace.write_carapace_spec(command, target, prog_name=None, *, invocation=None)[source]¶
Render the spec and write it to
target, returning the written path.Creates parent directories as needed.
invocationis recorded in the header comment (seedump_carapace_spec()).- Return type:
- click_extra.carapace.install_carapace_spec(command, prog_name=None, *, invocation=None)[source]¶
Write the spec into Carapace’s user spec directory.
Targets
$XDG_CONFIG_HOME/carapace/specs/<prog>.yaml(honoring an explicitXDG_CONFIG_HOME), which Carapace loads on startup. Returns the path.- Return type:
- class click_extra.carapace.CarapaceComplete(cli, ctx_args, prog_name, complete_var)[source]¶
Bases:
ShellCompleteClick completion backend that emits Carapace’s value/description lines.
Registered as the
carapaceshell, so_FOO_COMPLETE=carapace_completemakes a Click CLI print completions in thevalue\tdescriptiontext format Carapace’s shell macro parses. This is the callback target of the dynamic actions emitted by_dynamic_action(); it reuses Click’s ownget_completions(), so a parameter’s customshell_completeis honored verbatim.The current word is left to Carapace to filter, so completion args are the already-typed words with an empty incomplete value.
- name: ClassVar[str] = 'carapace'¶
Shell name Click registers this backend under (the
carapace_completecompletion instruction).
- source_template: ClassVar[str] = ''¶
Completion script template formatted by
source(). This must be provided by subclasses.