cli_engine can behave differently depending on the name it was invoked as, in the style of
busybox or git's built-ins. A single binary, symlinked (or shimmed) under several names,
dispatches to different behavior based on argv[0].
This is entirely opt-in. An application that registers no alternative names behaves exactly as a binary that never knew about the feature, so adding it breaks nothing.
Register alternative names on CliConfig. Each name maps to one of two behaviors:
CliConfig::with_argv0_alias rewrites the invocation into canonical subcommand
tokens and runs it through the normal command tree, with the real argument tail appended.
use cli_engine::CliConfig;
let config = CliConfig::new("my-cli", "Team CLI", "my-cli")
// Invoked as `pl`, behave like `project list`.
.with_argv0_alias("pl", ["project", "list"]);Invoking the binary as pl --team platform is then identical to my-cli project list --team platform: same parsing, flags, auth, middleware, and output.
CliConfig::with_argv0_personality runs an entirely separate CLI built from its own
[CliConfig] — its own root name, commands, flags, and auth. The closure runs lazily, only when the
route is actually dispatched, so unused personalities cost nothing.
use cli_engine::CliConfig;
let config = CliConfig::new("my-cli", "Team CLI", "my-cli")
.with_argv0_personality("legacy-tool", || {
CliConfig::new("legacy-tool", "Legacy compatibility shim", "legacy-tool")
// ...modules, commands, auth for the legacy personality...
});The personality presents the name from its own CliConfig in help and usage output.
The engine never needs to know its own canonical name. It only matches argv[0] against the
registered alternative names. Anything else — the real binary name, or the binary renamed to
something unregistered — falls through to the default CLI unchanged. Renaming my-cli to
otherthing (not a registered route) just runs the normal my-cli application.
The Hidden argv0 Command
cli_engine also accepts an undocumented argv0 command that forces a route without a symlink:
my-cli argv0 pl --team platform # dispatch as if invoked as `pl`
my-cli argv0 legacy-tool ... # dispatch the `legacy-tool` personalityIt is recognized as the first argument after the program name, is never registered with clap, and
so never appears in --help, tree, or --search. It is active only when the application has
registered at least one route.
Unlike the silent symlink fall-through, an explicit argv0 invocation is strict: an unknown
name, or a bare argv0 with no name, exits non-zero with an error listing the known names. This
keeps mistakes visible while leaving foreign scripts that merely call your binary untouched (nothing
triggers dispatch unless the literal argv0 token is present).
ln -s my-cli /usr/local/bin/pl
ln -s my-cli /usr/local/bin/legacy-toolargv[0] is the link name, so the engine matches its basename (path and any extension stripped,
e.g. .exe on Windows) against the registered routes.
Native links work the same as on Unix and need no shim. A soft symlink
(mklink pl.exe my-cli.exe) or hard link (mklink /H pl.exe my-cli.exe) sets argv[0] to the link
name, not the resolved target, so the engine matches it directly. The .exe extension is stripped,
so a link named pl.exe matches a route registered as pl:
mklink pl.exe my-cli.exe REM soft symlink (developer mode or elevated)
mklink /H legacy-tool.exe my-cli.exe REM hard link (no elevation needed)When links are inconvenient (e.g. no developer mode, or distributing across volumes where hard links
can't reach), a tiny .cmd shim beside the binary is the lightweight alternative. Because a launched
process cannot read the name of the .cmd that started it, the shim forwards its own filename to the
explicit argv0 command using the batch parameter %~n0 (the script's base name):
REM pl.cmd — the FILENAME is the alias; the body is generic boilerplate
@"%~dp0my-cli.exe" argv0 %~n0 %*%~dp0— the directory of the shim, so the siblingmy-cli.exeis found.%~n0— the shim's own base name (pl), supplied as the explicitargv0name.%*— the caller's arguments, forwarded unchanged.
The same one-line body works for every alias; only the file's name differs (pl.cmd,
legacy-tool.cmd, ...). Dispatch stays explicit, so the shim never triggers in unrelated scripts.
Nesting works correctly: if myscript.cmd calls pl, the dispatched name is pl, not myscript —
%~n0 is evaluated inside pl.cmd and always resolves to pl regardless of the caller.
You always register the bare name (with_argv0_alias("pl", ...)). The name resolved from a link or
shim has its extension stripped, so the same registration matches pl (Unix link), pl.exe
(Windows link), and pl.cmd (shim) alike — whether the shim forwards %~n0 (already extension-less)
or %~nx0/%0 (with the .cmd suffix).
Rather than authoring links by hand, installers and self-healing code can create them with
Cli::create_link. It takes the registered name, a directory, an optional target
executable (None uses the current executable), and an Argv0LinkMethod:
| Method | Unix | Windows |
|---|---|---|
SoftLink |
symbolic link <name> |
symbolic link <name>.exe (Developer Mode/elevation) |
HardLink |
hard link <name> |
hard link <name>.exe (same volume) |
Script |
executable <name> shell script |
<name>.cmd batch shim |
use cli_engine::Argv0LinkMethod;
// Install (or restore) every registered alternative name into the bin directory,
// pointing at the running binary. Pick the method the platform/installer prefers.
let method = if cfg!(windows) {
Argv0LinkMethod::Script
} else {
Argv0LinkMethod::SoftLink
};
for name in cli.argv0_names() {
cli.create_link(name, &bin_dir, None, method)?;
}create_link ensures the desired state idempotently: a destination that already matches is left
untouched, while a missing, wrong-target, or corrupted one is created or replaced — so re-running it
restores both deleted and broken links. Registered names must be simple [A-Za-z0-9_-] tokens that
differ from the CLI's own name.