Skip to content

Support invocation-name-aware CLIs (multicall / argv[0] dispatch) #1619

@kdeldycke

Description

@kdeldycke

Summary

Add first-class support for CLIs that change behavior based on how they are invoked (their argv[0] / symlink name). This is the "multicall" pattern, used by BusyBox, bzip2/bunzip2, gunzip, vim/vi/view, exiftool, and others. Click-extra already has sophisticated prog_name handling; extending it to let commands inspect and dispatch on the invocation name is a natural next step.

Motivation

Programs that read their own invocation name to select behavior are a long-standing Unix tradition:

  • BusyBox: a single binary, hundreds of applets selected by argv[0] via symlinks.
  • bzip2 / bunzip2 / bzcat: same binary, mode chosen by name. Same pattern for gzip/gunzip/zcat, xz/unxz/lzcat.
  • vim / vi / view / vimdiff: different default options depending on the invoked name.
  • exiftool: supports parenthesized options embedded in the filename, e.g. exiftool(-k) is equivalent to exiftool -k.
  • pigz / unpigz, pkg-config variants: hardlinks to the same inode, different names.

A recent proposal pushes the idea further, suggesting that entire configurations could be encoded in the filename (e.g. train---resnet50---lr0.001---batch32.exe). The Hacker News discussion surfaced practical observations:

  • Filenames persist on the filesystem, unlike shell history or ephemeral flags. A renamed binary is a self-documenting, shareable, zero-setup invocation.
  • The pattern works best for a small, fixed set of named personalities (bunzip2, vi vs. vim) rather than arbitrary parameter encoding, which runs into filename length limits (255 chars), special-character restrictions, discoverability problems, and version-control friction.
  • Symlinks eliminate the "duplicate binary" concern.
  • The traditional multicall pattern (dispatch to a subcommand) is distinct from the "filename-as-config" pattern (parse arbitrary key-value pairs from the name). Both are worth supporting, but the former is far more common and less controversial.

Proposed scope

1. Multicall group (primary feature)

A new decorator or Group subclass that inspects sys.argv[0] (resolved through symlinks) and dispatches to a subcommand matching the invocation name:

from click_extra import multicall_group, command

@multicall_group()
def toolkit():
    """A BusyBox-style multicall binary."""

@toolkit.command()
def compress():
    """Compress files."""
    ...

@toolkit.command()
def decompress():
    """Decompress files."""
    ...

When invoked as toolkit, it works as a normal group with subcommands. When invoked via a symlink named compress, it skips the group and runs the compress command directly.

Key behaviors:

  • Symlink resolution: detect whether argv[0] basename matches a registered subcommand. If so, dispatch directly.
  • Fallback: if the name doesn't match any subcommand, fall through to normal group behavior (show help, require a subcommand argument).
  • --help still works: compress --help shows help for the compress subcommand, not the group.
  • Listing personalities: toolkit --list-personalities (or similar) enumerates all available symlink names.
  • Installer helper: optionally generate symlinks or shell aliases for all subcommands in a given directory.

2. Invocation-name hook (secondary feature)

Expose the raw invocation name to any command via the Click context, so authors can implement custom dispatch logic:

from click_extra import command, pass_context

@command()
@pass_context
def my_tool(ctx):
    invoked_as = ctx.meta["invocation_name"]  # or a dedicated context attribute
    if invoked_as == "bunzip2":
        # decompress mode
        ...

This is lower-level and lets users build their own patterns without the full multicall group machinery.

3. Name-embedded options (optional, exploratory)

Support for exiftool-style parenthesized flags in the binary name, e.g. mytool(-v)(--format=json). This is a niche feature and could be deferred, but it fits naturally into the argv[0]-inspection infrastructure.

Design considerations

  • prog_name interaction: click-extra already overrides prog_name in ExtraCommand.main(). The multicall logic should integrate with this, not fight it. When dispatching via symlink, prog_name should reflect the symlink name.
  • Windows: symlinks require elevated privileges on older Windows. Hardlinks or .cmd wrappers are the practical alternative. The feature should degrade gracefully.
  • Entry points: Python packaging entry points (via [project.scripts]) already create named wrappers. The multicall pattern is complementary: one entry point for the group, symlinks for the personalities.
  • Testing: ExtraCliRunner should support simulating invocation under a different argv[0] without actual symlinks on disk.

Prior art in other frameworks

  • Click itself has no multicall support. CommandCollection merges multiple groups but doesn't dispatch on argv[0].
  • argparse has no built-in support.
  • Rust's clap: no direct multicall, but the pattern is commonly hand-rolled.
  • Go's cobra: no built-in multicall.

This would be a differentiating feature for click-extra.

Non-goals

  • Arbitrary key-value parsing from filenames (the train---resnet50---lr0.001 pattern). The HN discussion consensus is that this is fragile, hard to discover, breaks tooling expectations, and is better served by config files or wrapper scripts.
  • Self-modifying executables or filename-embedded encrypted configs.
  • Replacing standard --flags for normal CLI usage. This feature is about dispatch, not a wholesale alternative to argument parsing.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    ✨ enhancementNew feature or improvement to an existing one

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions