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 a ParamType that overrides shell_complete) cannot be frozen, so its spec action calls back into the CLI. The callback reuses Click’s own completion machinery through the carapace completion class, registered on import 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:

  1. to_carapace_spec(cli, prog_name=...) returns the spec as a plain dict, ready for yaml.safe_dump or further processing. It needs no optional dependency.

  2. dump_carapace_spec(cli, prog_name=...) serializes that dict to a YAML string with a provenance header.

  3. write_carapace_spec(cli, target, prog_name=...) writes the YAML to a path, and install_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) becomes exclusiveflags.

  • The dynamic callback hands the already-typed words to Carapace and lets it filter, so a parameter whose shell_complete does 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 a ParamType that overrides shell_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 via CarapaceComplete.

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>.yaml here (see install_carapace_spec()) is all it takes for Carapace to pick up a CLI’s completions. Mirrors the $XDG_CONFIG_HOME default documented by Carapace; an explicit XDG_CONFIG_HOME is honored by install_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: object

The completion block of a command: per-flag and positional actions.

flag: dict[str, list[str]]

Map of flag name (long spelling, no dashes) to its completion action.

positional: list[list[str]]

Per-position completion actions for fixed-arity arguments, in order.

positionalany: list[str]

Completion action repeated for every trailing variadic argument.

is_empty()[source]

Whether nothing here is worth serializing.

Return type:

bool

to_dict()[source]

Serialize to the carapace-spec completion mapping, dropping empty members and trailing empty positional slots.

Return type:

dict

class click_extra.carapace.CarapaceCommand(name, description='', aliases=(), hidden=False, flags=<factory>, persistentflags=<factory>, exclusiveflags=<factory>, completion=<factory>, commands=<factory>)[source]

Bases: object

One node of a Carapace spec: a command and its flags, completions and subcommands, mirroring the upstream Command schema.

name: str

The command’s invocation name (the root carries the program name).

description: str = ''

One-line command description (NAME-section short help).

aliases: tuple[str, ...] = ()

Alternative names the command also answers to (from Cloup).

hidden: bool = False

Whether the command is hidden from listings (still completable).

flags: dict[str, str]

Map of flag key (with shape suffixes) to its description.

persistentflags: dict[str, str]

Flags inherited by every subcommand (the root’s default option set).

exclusiveflags: list[list[str]]

Groups of mutually-exclusive flag names (from Cloup constraints).

completion: CarapaceCompletion

Static and dynamic completion actions for this command’s parameters.

commands: list[CarapaceCommand]

Nested subcommands.

to_dict()[source]

Serialize to a carapace-spec command mapping, dropping empties.

Only name is required by the schema, so every other member is omitted when empty to keep the YAML compact.

Return type:

dict

click_extra.carapace.extract_carapace_command(command, ctx, *, is_root, default_opts, inherited_opts, root_name)[source]

Build a CarapaceCommand from a Click command and its context.

The context must have been created for command (typically via click.Command.make_context() with resilient_parsing=True). Subcommands are discovered dynamically and recursed into.

default_opts is the abstract set of spellings Click Extra injects on every command; on the root, options drawn from it become persistentflags. inherited_opts is 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_name is the binary Carapace dispatches on, used to build dynamic callback macros.

Return type:

CarapaceCommand

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 ctx when given (the live invocation context), otherwise builds a throwaway one with resilient_parsing=True. The returned mapping conforms to the carapace-spec schema and is ready to hand to yaml.safe_dump.

Return type:

dict

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, the invocation command line that produced it (when given), and a link to the documentation.

Return type:

str

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. invocation is recorded in the header comment (see dump_carapace_spec()).

Return type:

Path

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 explicit XDG_CONFIG_HOME), which Carapace loads on startup. Returns the path.

Return type:

Path

class click_extra.carapace.CarapaceComplete(cli, ctx_args, prog_name, complete_var)[source]

Bases: ShellComplete

Click completion backend that emits Carapace’s value/description lines.

Registered as the carapace shell, so _FOO_COMPLETE=carapace_complete makes a Click CLI print completions in the value\tdescription text format Carapace’s shell macro parses. This is the callback target of the dynamic actions emitted by _dynamic_action(); it reuses Click’s own get_completions(), so a parameter’s custom shell_complete is 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_complete completion instruction).

source_template: ClassVar[str] = ''

Completion script template formatted by source(). This must be provided by subclasses.

source()[source]

No-op source step: Carapace, not a shell, consumes this backend.

Return type:

str

get_completion_args()[source]

Read the words Carapace passed via COMP_WORDS (program name first).

Returns the typed arguments with an empty incomplete value; Carapace applies its own prefix filtering to the candidates we return.

Return type:

tuple[list[str], str]

format_completion(item)[source]

Render one completion as Carapace’s value or value\tdescription.

Return type:

str