Python Click multiple command names
Asked Answered
D

5

12

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?

Derayne answered 9/10, 2017 at 8:29 Comment(2)
Can you show the command lines you are trying to construct?Lenka
It must be like mycli my-command and mycli my-cmd for the same command/functionCharette
S
4

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
Stope answered 5/4, 2022 at 14:38 Comment(0)
L
14

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:

Custom Class

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

Using the Custom Class

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():
    ....

Test Code:

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())

Test Results:

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
Lenka answered 13/10, 2017 at 1:0 Comment(1)
I saw this was over 6 years old... I thought surly, it's been added by now... I look at the documentation page and... still "not supported out of the box" click.palletsprojects.com/en/8.1.x/advanced/#command-aliasesKellar
N
11

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,
}
Neva answered 4/11, 2018 at 19:27 Comment(0)
S
4

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
Stope answered 5/4, 2022 at 14:38 Comment(0)
T
2

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:

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

Usage:

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()

Output:

> 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.
Tessietessier answered 5/7, 2023 at 22:45 Comment(0)
S
2

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
Saucier answered 21/2 at 13:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.