Reputation: 175
Is it possible to do something like this with Python Click?
@click.command(name=['my-command', 'my-cmd'])
def my_command():
pass
I want my command lines to be something like:
mycli my-command
and
mycli my-cmd
but reference the same function.
Do I need to do a class like AliasedGroup?
Upvotes: 13
Views: 9217
Reputation: 1
My solution:
class AliasedCommandGroup(click.Group):
def __init__(self, *args, **kwargs):
self._aliases = dict()
super().__init__(*args, **kwargs)
def add_command(self, cmd, name = None, alias = None):
if alias is not None:
if not isinstance(alias, str):
raise RuntimeError("Invalid alias.")
if alias in self._aliases:
raise RuntimeError("Alias already exists.")
self._aliases.update({alias: name or cmd.name})
return super().add_command(cmd, name)
def get_command(self, ctx, cmd_name):
cmd = super().get_command(ctx, cmd_name)
if cmd is not None:
return cmd
if cmd_name in self._aliases:
return super().get_command(ctx, self._aliases[cmd_name])
return None
and then register command this way:
@click.group(cls=AliasedCommandGroup)
def cli_root():
pass
cli_root.add_command(some_command, alias="some_alias")
Upvotes: 0
Reputation: 167
well, I can't comment yet, but extending @Chris answer with just hiding the newly generated aliases with hidden=True
:
class CliGroup(RichGroup):
def command(self, *args, **kwargs):
"""Adds the ability to add `aliases` to commands."""
def decorator(f):
aliases = kwargs.pop("aliases", None)
if aliases and isinstance(aliases, list):
name = kwargs.pop("name", None)
if not name:
raise click.UsageError(
"`name` command argument is required when using aliases."
)
base_command = super(CliGroup, self).command(name, *args, **kwargs)(f)
for alias in aliases:
cmd = super(CliGroup, self).command(
alias, hidden=True, *args, **kwargs
)(f)
cmd.help = f"Alias for '{name}'.\n\n{cmd.help}"
cmd.params = base_command.params
else:
cmd = super(CliGroup, self).command(*args, **kwargs)(f)
return cmd
return decorator
Upvotes: 2
Reputation: 19835
Since this question has been asked, someone (not me) created a click-aliases
library.
It works a bit like the other answers except that you don’t have to declare the command class by yourself:
import click
from click_aliases import ClickAliasedGroup
@click.group(cls=ClickAliasedGroup)
def cli():
pass
@cli.command(aliases=['my-cmd'])
def my_command():
pass
Upvotes: 4
Reputation: 95
I tried @Stephan Rauch's solution and was met with some challenges like help text output so I expanded on it. This was before I saw there's a library for this so I haven't tried that as what I built is working the way I want it to.
Adds a aliases=['foo', 'bar']
argument to the command while copying the help information from the base command.
class CustomCliGroup(click.Group):
"""Custom Cli Group for Click"""
def command(self, *args, **kwargs):
"""Adds the ability to add `aliases` to commands."""
def decorator(f):
aliases = kwargs.pop("aliases", None)
if aliases and isinstance(aliases, list):
name = kwargs.pop("name", None)
if not name:
raise click.UsageError("`name` command argument is required when using aliases.")
base_command = super(CustomCliGroup, self).command(
name, *args, **kwargs
)(f)
for alias in aliases:
cmd = super(CustomCliGroup, self).command(alias, *args, **kwargs)(f)
cmd.help = f"Alias for '{name}'.\n\n{cmd.help}"
cmd.params = base_command.params
else:
cmd = super(CustomCliGroup, self).command(*args, **kwargs)(f)
return cmd
return decorator
import click
@click.group(
context_settings=dict(help_option_names=["-h", "--help"]), cls=CustomCliGroup
)
def cli():
"""My Excellent CLI"""
@cli.command()
def hello():
"""Says hello"""
click.echo("Hello, World!")
@cli.command(name="do", aliases=["stuff"])
@click.argument("name")
@click.option("--times", "-t", default=1, help="Number of times to do the thing")
def my_command(name, times):
"""This is my command"""
click.echo(f"Doing {name} {times} times.")
if __name__ == "__main__":
cli()
> python test.py -h
Usage: test.py [OPTIONS] COMMAND [ARGS]...
My Excellent CLI
Options:
-h, --help Show this message and exit.
Commands:
do This is my command
hello Says hello
stuff Alias for 'do'.
------------------------
> python test.py do -h
Usage: test.py do [OPTIONS] NAME
This is my command
Options:
-t, --times INTEGER Number of times to do the thing
-h, --help Show this message and exit.
------------------------
> python test.py stuff -h
Usage: test.py stuff [OPTIONS] NAME
Alias for 'do'.
This is my command
Options:
-t, --times INTEGER Number of times to do the thing
-h, --help Show this message and exit.
Upvotes: 2
Reputation: 8516
Here is a simpler way to solve the same thing:
class AliasedGroup(click.Group):
def get_command(self, ctx, cmd_name):
try:
cmd_name = ALIASES[cmd_name].name
except KeyError:
pass
return super().get_command(ctx, cmd_name)
@click.command(cls=AliasedGroup)
def cli():
...
@click.command()
def install():
...
@click.command()
def remove():
....
cli.add_command(install)
cli.add_command(remove)
ALIASES = {
"it": install,
"rm": remove,
}
Upvotes: 11
Reputation: 49794
AliasedGroup is not what you are after, since it allows a shortest prefix match, and it appears you need actual aliases. But that example does provide hints in a direction that can work. It inherits from click.Group
and overides some behavior.
Here is a one way to approach what you are after:
This class overides the click.Group.command()
method which is used to decorate command functions. It adds the ability to pass a list of command aliases. This class also adds a short help which references the aliased command.
class CustomMultiCommand(click.Group):
def command(self, *args, **kwargs):
"""Behaves the same as `click.Group.command()` except if passed
a list of names, all after the first will be aliases for the first.
"""
def decorator(f):
if isinstance(args[0], list):
_args = [args[0][0]] + list(args[1:])
for alias in args[0][1:]:
cmd = super(CustomMultiCommand, self).command(
alias, *args[1:], **kwargs)(f)
cmd.short_help = "Alias for '{}'".format(_args[0])
else:
_args = args
cmd = super(CustomMultiCommand, self).command(
*_args, **kwargs)(f)
return cmd
return decorator
By passing the cls
parameter to the click.group()
decorator, any commands added to the group via the the group.command()
can be passed a list of command names.
@click.group(cls=CustomMultiCommand)
def cli():
"""My Excellent CLI"""
@cli.command(['my-command', 'my-cmd'])
def my_command():
....
import click
@click.group(cls=CustomMultiCommand)
def cli():
"""My Excellent CLI"""
@cli.command(['my-command', 'my-cmd'])
def my_command():
"""This is my command"""
print('Running the command')
if __name__ == '__main__':
cli('--help'.split())
Usage: my_cli [OPTIONS] COMMAND [ARGS]...
My Excellent CLI
Options:
--help Show this message and exit.
Commands:
my-cmd Alias for 'my-command'
my-command This is my command
Upvotes: 14