Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command aliases #132

Closed
max-block opened this issue Jul 3, 2020 · 12 comments
Closed

Command aliases #132

max-block opened this issue Jul 3, 2020 · 12 comments
Labels
answered feature New feature, enhancement or request

Comments

@max-block
Copy link

It would be nice to have command aliases, when all these commands do the same thing:

my-app delete
my-app uninstall
my-app d

There is a module for click which can do it: https://github.com/click-contrib/click-aliases

Code looks like this:

import click
from click_aliases import ClickAliasedGroup


@click.group(cls=ClickAliasedGroup)
def cli():
    pass


@cli.command(aliases=["i", "inst"])
def install():
    click.echo("install")


@cli.command(aliases=["d", "uninstall", "remove"])
def delete():
    click.echo("delete")

Also this module click-aliases add info about alises to help:

Usage: t3 [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  delete (d,remove,uninstall)
  install (i,inst)
@max-block max-block added the feature New feature, enhancement or request label Jul 3, 2020
@tiangolo
Copy link
Member

Hey, as Typer doesn't modify the function, and only "registers" it, you could do this:

import typer

app = typer.Typer()

@app.command("delete")
@app.command("uninstall")
@app.command("d")
def delete(name: str):
    typer.echo(f"Deleting {name}")


if __name__ == "__main__":
    app()

@ssbarnea
Copy link
Contributor

ssbarnea commented Apr 5, 2021

Manually adding aliases does not really scale well. Click has some code for implementing git-like behavior at https://click.palletsprojects.com/en/7.x/advanced/ but the tricky bit is that I have no idea on how to adapt this code to work for typer, especially as I do generate commands dynamically at https://github.com/pycontribs/mk/blob/main/src/mk/__main__.py#L87-L91

@tddschn
Copy link

tddschn commented Apr 13, 2022

Hey, as Typer doesn't modify the function, and only "registers" it, you could do this:

import typer

app = typer.Typer()

@app.command("delete")
@app.command("uninstall")
@app.command("d")
def delete(name: str):
    typer.echo(f"Deleting {name}")


if __name__ == "__main__":
    app()

@tiangolo
This doesn't work as intended if the user invokes the app with --help:

...
Commands:
  a         Add a new to-do with a DESCRIPTION.
  add       Add a new to-do with a DESCRIPTION.

What I want is:

...
Commands:
  add, a         Add a new to-do with a DESCRIPTION.

How do I do this with typer? Thank you.

@misha
Copy link

misha commented Jul 20, 2022

@tddschn I solved this by hiding the subsequent option, works pretty well.

@app.command(no_args_is_help=True, help='Open a log stream to a service. (also "logs")')
@app.command('logs', hidden=True)
def log(
  service: Service = typer.Argument(..., help='The service whose logs to stream.'),
):
  docker_execute(['logs', '-f', service])

@ssbarnea
Copy link
Contributor

ssbarnea commented Mar 6, 2023

I wonder if we could use https://click.palletsprojects.com/en/8.1.x/advanced/ in with typer as being able to just catch an unknown command and redirect it to the correct one is far more flexible and could also allow us to address typos by avoiding pre-generating all possible values in advance, which is quite ugly.

@FergusFettes
Copy link

FergusFettes commented May 12, 2023

I have also been looking for a solution to this, tried a few different methods, none of them worked nicely. The best one is just adding hidden commands:

@app.command()
@app.command(name="f", hidden=True)
def foobar(hello: str):
    "Foobar command"
    print("foobar with hello: ", hello)

but then we don't get a list of aliases, and you cant run something like alias f foobar

If someone more experienced than me would give a pointer for how a function that does alias f foobar that would be lovely.

@FFengIll
Copy link

FFengIll commented May 13, 2023

I always use bellow for alias (exactly, command is a decorator, so it is a caller too).
Furthermore, you can do bellow before app() as a statement.

#  alias, it also use the decorator's logic and work well
app.command(name="byebye", help="alias of goodbye")(goodbye)

@FergusFettes
Copy link

FergusFettes commented May 16, 2023

@FFengIll thats neat but its only available on build right?

what i want is something like

@app.command()
def alias(ctx: Context, name: str, command: str):
    aliases = ctx.obj.get('aliases', None)
    command = ctx.parent.command.get_command(ctx, command)
    if aliases and command:
        aliases[name] = command

    app.command(name=name)(command)     # <-- this doesn't work because i cant get the global app obj? i also tried adding it to the context and running ctx.obj.app.... but it didn't work either

@app.command()
def unalias(....

that i can use from within a session..

@gar1t
Copy link

gar1t commented Sep 11, 2023

You can support this with a tweak to the Group class:

class AliasGroup(typer.core.TyperGroup):

    _CMD_SPLIT_P = re.compile(r", ?")

    def get_command(self, ctx, cmd_name):
        cmd_name = self._group_cmd_name(cmd_name)
        return super().get_command(ctx, cmd_name)

    def _group_cmd_name(self, default_name):
        for cmd in self.commands.values():
            if cmd.name and default_name in self._CMD_SPLIT_P.split(cmd.name):
                return cmd.name
        return default_name

Usage:

app = typer.Typer(cls=AliasGroup)

@app.command("foo, f")
def foo():
    """Print a message and exit."""
    print("Works as command 'foo' or its alias 'f'")

@app.callback()
def main():
    pass

app()

See the full Gist.

One could imagine more explicit support of this with a mod to the upstream group class and a new aliases arg to the command decorator.

app = Typer()  # Uses TyperGroup, which is modified to look for an 'aliases' attr on commands

@app.command("foo", aliases=["f"])
def foo():
    pass

@mrharpo
Copy link

mrharpo commented Nov 1, 2023

Thank you, @gar1t ! This works great!

+1 for command aliases=[]

It would be nice if it could include a custom parsing separator, and display separator override. E.g. I like to use | for command separation in my help text:

b | build     Build the dev environment
c | cmd       Run a command inside the dev environment
d | dev       Run the dev environment
m | manage    Run a manage.py function
s | shell     Enter into a python shell inside the dev environment
t | tui       Run an interactive TUI

@mrharpo
Copy link

mrharpo commented Nov 1, 2023

@gar1t A slightly more robust solution that allows for multiple (or sloppy) delimiters:

class AliasGroup(TyperGroup):

    _CMD_SPLIT_P = r'[,| ?\/]' # add other delimiters inside the [ ]

    ...

    def _group_cmd_name(self, default_name):
        for cmd in self.commands.values():
            if cmd.name and default_name in re.split(self._CMD_SPLIT_P, cmd.name):
                return cmd.name
        return default_name

@rnag
Copy link

rnag commented Oct 16, 2024

@mrharpo Not really a typo, but a feeling of improvement for the regex (or maybe I didn't really understand the \/ part) could be something like:

r" ?[,|] ?"

My full code is like:

class AliasGroup(TyperGroup):

    _CMD_SPLIT_P = re.compile(r" ?[,|] ?")

    def get_command(self, ctx, cmd_name):
        cmd_name = self._group_cmd_name(cmd_name)
        return super().get_command(ctx, cmd_name)

    def _group_cmd_name(self, default_name):
        for cmd in self.commands.values():
            name = cmd.name
            if name and default_name in self._CMD_SPLIT_P.split(name):
                return name
        return default_name


app = typer.Typer(cls=AliasGroup)

@app.command("a | action | xyz")
def do_something():
    """
    Some description here.
    """
   ...

I agree the help text is certainly improved 🎉 :

 Usage: cmd [OPTIONS] COMMAND [ARGS]...

╭─ Options ──────────────────────────────────────────────────────────────────────────╮
│ --install-completion          Install completion for the current shell.            │
│ --show-completion             Show completion for the current shell, to copy it or │
│                               customize the installation.                          │
│ --help                        Show this message and exit.                          │
╰────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ─────────────────────────────────────────────────────────────────────────╮
│ a | action | xyz   Some description here.                                          │
╰────────────────────────────────────────────────────────────────────────────────────╯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
answered feature New feature, enhancement or request
Projects
None yet
Development

No branches or pull requests

10 participants