pitosalas
pitosalas

Reputation: 10862

Python Click - name synonyms or abbreviations

I am using python click to implement a cli. Simplest example would allow me to say:

./mytest.py help

but also

./mytest.py h

In other words, the help command can be shortened to just h. This is the way many cli programs work, e.g. git.

From what I can tell the code snippet:

@click.command()
def help():
    click.echo("Help!)

Will allow me to have:

./mytest.py help

But I don't know how to allow the synonym. I can't find anything that explains this.

Upvotes: 3

Views: 753

Answers (2)

Mark
Mark

Reputation: 4455

I use the context forward method.

$ cat cli.py
#!/usr/bin/env python
import click

@click.group()
def cli():
  """
  cli commands 
  """

@cli.command()
def help():
  """
  Get help
  """
  click.echo("Help!")

@cli.command("h")
@click.pass_context
def another_help(ctx):
  """
  Another way of asking for help
  """
  ctx.forward(help)

if __name__ == "__main__":
    cli()

And here's a test run:

$ ./cli.py
Usage: cli.py [OPTIONS] COMMAND [ARGS]...

  cli commands

Options:
  --help  Show this message and exit.

Commands:
  h     Another way of asking for help
  help  Get help

$ ./cli.py h
Help!
$ ./cli.py help
Help!

For whatever reason, I also demonstrated that the command name and function name can be different: I named the synonym function another_help but I told click the command is h.

The real issue with my approach is that the help text associated with the synonym must be copied, too. More importantly, the parameters (that is, options and arguments) also have to be copied.

In essence, the ctx.forward() approach increases the maintenance burden. It is easy to get them out of sync (let's say as you added additional options or arguments).

As you can see in the test run, the same command also gets listed twice. And so, I often just hide the synonym so that the list of commands is clearer. Plus, I don't have to keep the help text (Another way of asking for help) in sync.

So, here's an improved version, with the h synonym hidden:

$ cat cli.py
#!/usr/bin/env python
import click

@click.group()
def cli():
  """
  cli commands 
  """

@cli.command()
def help():
  """
  Get help
  """
  click.echo("Help!")

@cli.command("h",hidden=True)
@click.pass_context
def another_help(ctx):
  """
  Another way of asking for help
  """
  ctx.forward(help)

if __name__ == "__main__":
    cli()


$ ./cli.py 
Usage: cli.py [OPTIONS] COMMAND [ARGS]...

  cli commands

Options:
  --help  Show this message and exit.

Commands:
  help  Get help
$ ./cli.py h
Help!

Finally, here's the "parameter problem": if you add a parameter to one function, you have to add the same parameter to the synonym.

In the example below, I added a --name option to the help command, but not the h synonym:

$ cat cli.py
#!/usr/bin/env python
import click

@click.group()
def cli():
  """
  cli commands 
  """

@cli.command()
def help():
  """
  Get help
  """
  click.echo("Help!")

@cli.command("h",hidden=True)
@click.pass_context
def another_help(ctx):
  """
  Another way of asking for help
  """
  ctx.forward(help)

if __name__ == "__main__":
    cli()
$ vi cli.py
$ ./cli.py 
Usage: cli.py [OPTIONS] COMMAND [ARGS]...

  cli commands

Options:
  --help  Show this message and exit.

Commands:
  help  Get help
$ ./cli.py help Fred
Help, Fred!
$ ./cli.py h Fred
Usage: cli.py h [OPTIONS]
Try 'cli.py h --help' for help.

Error: Got unexpected extra argument (Fred)

Above, you can see that the h command has a problem with the argument. And so, to correct this, I would also have to add name to the another_help function like this:

@cli.command("h",hidden=True)
@click.pass_context
@click.argument("name",required=False)
def another_help(ctx,name):
  """
  Another way of asking for help
  """
  ctx.forward(help)

And that's what I mean about the maintenance burden - adding parameters or options has to be done to both synonyms.

Upvotes: 1

jrc
jrc

Reputation: 21901

In the Click documentation, this is known as "aliases".

The documentation at https://click.palletsprojects.com/en/8.x/advanced/ provides a code snippet for what you're looking for. Additionally, it links to sample code at https://github.com/pallets/click/tree/master/examples/aliases.

Here is a minimal implementation for the example given:

import click


class AliasedGroup(click.Group):
    def get_command(self, ctx, cmd_name):
        if cmd_name in ["help", "h"]:
            return click.Group.get_command(self, ctx, "help")
        return None


@click.command(cls=AliasedGroup)
def cli():
    pass


@cli.command()
def help():
    click.echo("Help!")


if __name__ == "__main__":
    cli()

Upvotes: 2

Related Questions