Multicall binariesยถ
Some of the oldest tools on Unix are one file answering to many names: bzip2, bunzip2 and bzcat are the same binary, vim and view differ by default options, and BusyBox multiplexes hundreds of applets behind symlinks pointing at a single executable. The behavior is selected by the invocation name: the argv[0] the process starts under, which a symlink or a hard link is free to change without touching the file itself.
MulticallGroup brings the pattern to Click Extra: a group that, when invoked under the name of one of its subcommands, skips the group entirely and behaves exactly like that subcommand as a standalone binary. Neither Click nor Cloup ships anything comparable, and on the Rust side clap has had first-class multicall since 3.2, which this design borrows from.
Declaring a multicall groupยถ
Use multicall_group() in place of group(); subcommands are declared the usual way:
from click_extra import argument, echo, multicall_group, option
@multicall_group()
def kitchen():
"""A multicall kitchen appliance."""
@kitchen.command()
@option("--temperature", default="180")
@argument("dishes", nargs=-1)
def bake(temperature, dishes):
"""Bake dishes in the oven."""
echo(f"Baking at {temperature} degrees: {', '.join(dishes) or 'nothing'}.")
@kitchen.command()
@option("--hours", default="2")
@argument("bottles", nargs=-1)
def chill(hours, bottles):
"""Chill bottles in the fridge."""
echo(f"Chilling for {hours} hours: {', '.join(bottles) or 'nothing'}.")
Invoked under its own name, the CLI is a regular group:
$ kitchen --help
Usage: kitchen [OPTIONS] COMMAND [ARGS]...
A multicall kitchen appliance.
Options:
--time / --no-time Measure and print elapsed execution time.
[default: no-time]
--config CONFIG_PATH Location of the configuration file. Supports
local path with glob patterns or remote URL.
[default: ~/.config/kitchen/{*.toml,*.yaml,*.yml,
*.json,*.json5,*.jsonc,*.hjson,*.ini,*.xml,*.plis
t,*.sqlite,*.sqlite3,*.conf,pyproject.toml}]
--no-config Ignore all configuration files and only use
command line parameters and environment
variables.
--validate-config FILE Validate the configuration file and exit.
--export-config FORMAT Export the configuration in the selected format
to <stdout>, then exit.
--accessible Accessibility mode: disable colors and render
tables in a borderless, screen-reader-friendly
format.
--color [auto|always|never] Colorize the output. A bare --color is the same
as --color=always. [default: auto]
--no-color Disable colorization (alias of --color=never).
--progress / --no-progress Show progress indicators during long operations.
Disabled for non-interactive output (pipes, dumb
terminals, CI) and by --accessible. [default:
progress]
--theme [auto|dark|dracula|light|manpage|monokai|nord|solarized_dark]
Color theme used for help screens. [default:
dark]
--params Show all CLI parameters, their provenance,
defaults and value, then exit.
--table-format [aligned|asciidoc|colon-grid|csv|csv-excel|csv-excel-tab|csv-unix|double-grid|double-outline|fancy-grid|fancy-outline|github|grid|heavy-grid|heavy-outline|hjson|html|jira|json|json5|jsonc|latex|latex-booktabs|latex-longtable|latex-raw|mediawiki|mixed-grid|mixed-outline|moinmoin|orgtbl|outline|pipe|plain|presto|pretty|psql|rounded-grid|rounded-outline|rst|simple|simple-grid|simple-outline|textile|toml|tsv|unsafehtml|vertical|xml|yaml|youtrack]
Rendering style of tables. [default: rounded-
outline]
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
[default: WARNING]
-v, --verbose Increase the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
-q, --quiet Decrease the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
--tree Show the tree of nested subcommands and exit.
--man Read the command's manual page and exit.
--help-format [carapace|json|json-full|man|markdown|markdown-full]
Render the command in the given format and exit.
--version Show the version and exit.
-h, --help Show this message and exit.
Commands:
bake Bake dishes in the oven.
chill Chill bottles in the fridge.
help Show help for a command.
personalities List the invocation names this binary answers to.
$ kitchen bake --temperature 200 pie
Baking at 200 degrees: pie.
Invoked under the name of one of its subcommands, it is that subcommand. CliRunner simulates the invocation name with its prog_name parameter, which stands in for the symlink below:
$ bake --help
Usage: bake [OPTIONS] [DISHES]...
Bake dishes in the oven.
Options:
--temperature TEXT [default: 180]
--time / --no-time Measure and print elapsed execution time.
[default: no-time]
--config CONFIG_PATH Location of the configuration file. Supports
local path with glob patterns or remote URL.
[default: ~/.config/bake/{*.toml,*.yaml,*.yml,*.j
son,*.json5,*.jsonc,*.hjson,*.ini,*.xml,*.plist,*
.sqlite,*.sqlite3,*.conf,pyproject.toml}]
--no-config Ignore all configuration files and only use
command line parameters and environment
variables.
--validate-config FILE Validate the configuration file and exit.
--export-config FORMAT Export the configuration in the selected format
to <stdout>, then exit.
--accessible Accessibility mode: disable colors and render
tables in a borderless, screen-reader-friendly
format.
--color [auto|always|never] Colorize the output. A bare --color is the same
as --color=always. [default: auto]
--no-color Disable colorization (alias of --color=never).
--progress / --no-progress Show progress indicators during long operations.
Disabled for non-interactive output (pipes, dumb
terminals, CI) and by --accessible. [default:
progress]
--theme [auto|dark|dracula|light|manpage|monokai|nord|solarized_dark]
Color theme used for help screens. [default:
dark]
--params Show all CLI parameters, their provenance,
defaults and value, then exit.
--table-format [aligned|asciidoc|colon-grid|csv|csv-excel|csv-excel-tab|csv-unix|double-grid|double-outline|fancy-grid|fancy-outline|github|grid|heavy-grid|heavy-outline|hjson|html|jira|json|json5|jsonc|latex|latex-booktabs|latex-longtable|latex-raw|mediawiki|mixed-grid|mixed-outline|moinmoin|orgtbl|outline|pipe|plain|presto|pretty|psql|rounded-grid|rounded-outline|rst|simple|simple-grid|simple-outline|textile|toml|tsv|unsafehtml|vertical|xml|yaml|youtrack]
Rendering style of tables. [default: rounded-
outline]
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
[default: WARNING]
-v, --verbose Increase the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
-q, --quiet Decrease the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
--tree Show the tree of nested subcommands and exit.
--man Read the command's manual page and exit.
--help-format [carapace|json|json-full|man|markdown|markdown-full]
Render the command in the given format and exit.
--version Show the version and exit.
-h, --help Show this message and exit.
The personality is not the subcommand wearing a different name: it is a standalone command carrying the groupโs options merged into the subcommandโs. All of them parse in one flat pass, in any order, with no subcommand token in sight:
$ bake --verbosity INFO --temperature 200 pie
Baking at 200 degrees: pie.
$ bake pie --temperature 200
Baking at 200 degrees: pie.
$ chill
Chilling for 2 hours: nothing.
On a real system the name comes from argv[0]: create a symlink (or a hard link) next to the entry point, on your $PATH, bearing the subcommandโs name:
ln -s "$(which kitchen)" "$(dirname "$(which kitchen)")/bake"
bake --temperature 200 pie
An invocation name matching no personality is not an error: it falls through to regular group behavior. That is also what keeps the feature inert under test runners and interpreters, where argv[0] is the runnerโs own binary. On Windows, entry points are .exe shims and the suffix is stripped before matching, so a bake.exe wrapper still dispatches.
Personalities with preset optionsยถ
A personality maps to a sequence of tokens, not just a subcommand: the first token names the subcommand, and the rest is prepended to the userโs arguments. That is how bzcat can be bzip2 --decompress --stdout, or view a vim that starts read-only:
from click_extra import argument, echo, multicall_group, option
@multicall_group(personalities={"quick-chill": ("chill", "--hours", "1")})
def fridge():
"""Multicall fridge with a preset personality."""
@fridge.command()
@option("--hours", default="2")
@argument("bottles", nargs=-1)
def chill(hours, bottles):
"""Chill bottles in the fridge."""
echo(f"Chilling for {hours} hours: {', '.join(bottles) or 'nothing'}.")
$ quick-chill sparkling-water
Chilling for 1 hours: sparkling-water.
An explicit personalities mapping is exhaustive: only the names it declares dispatch, and the identity mapping every subcommand would otherwise get is dropped. Declare them all if you want them all.
Listing the personalitiesยถ
The auto-injected personalities subcommand enumerates every name the binary answers to, next to the command line each one invokes:
$ fridge personalities
โญโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Name โ Invokes โ Description โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ quick-chill โ chill --hours 1 โ Chill bottles in the fridge. โ
โฐโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
It renders through the usual table machinery, so --table-format applies. Pass personalities_command=False to @multicall_group() to suppress it, or register your own personalities subcommand to replace it.
The invocation-name hookยถ
For custom dispatch logic that does not fit the group machinery, every command exposes the name it was invoked under in ctx.meta, under the click_extra.context.INVOCATION_NAME key. It equals Clickโs root info_name: the entry-point script name, the symlink name in personality mode, or an explicit prog_name override:
from click_extra import command, context, echo, pass_context
@command
@pass_context
def appliance(ctx):
"""Adapt its behavior to the name it was invoked under."""
invoked_as = ctx.meta[context.INVOCATION_NAME]
if invoked_as == "toaster":
echo("Browning bread.")
else:
echo(f"Running as {invoked_as}.")
$ toaster
Browning bread.
$ appliance
Running as appliance.
Behavior notesยถ
A personality is a genuine standalone binary, with the consequences that follow:
The groupโs callback does not run. The personality has no parent context. A group whose callback performs setup must fold that setup into the subcommands, or stay a plain group.
Namespaces follow the personality name. Configuration is read from the personalityโs own app dir (a
bakeinvocation reads what a standalonebakebinary would, not the groupโskitchendirectory), and the auto-generated environment variables carry the personality prefix (BAKE_TEMPERATURE). Akitchen bakeinvocation keeps the groupโs namespaces.Completion is keyed on the program name. Each personality needs its own shell-completion registration (a
_{NAME}_COMPLETEvariable, or its own carapace spec); the groupโs spec does not cover them.Eager options answer to the personality.
--help,--version,--params,--manand their siblings render for the personality alone:bake --versionnamesbake.