14

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?

2
  • Can you show the command lines you are trying to construct? Commented Oct 11, 2017 at 22:54
  • It must be like mycli my-command and mycli my-cmd for the same command/function Commented Oct 12, 2017 at 20:57

6 Answers 6

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
Sign up to request clarification or add additional context in comments.

1 Comment

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-aliases
12

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,
}

Comments

5

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

Comments

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.

Comments

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

Comments

0

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

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.