pythoncommand-line-interfacepython-click

Regular usage output with Click command aliases


I'm using this snippet for my custom group (from here) to allow prefixes.

class AliasedGroup(click.Group):
    def get_command(self, ctx, cmd_name):
        rv = click.Group.get_command(self, ctx, cmd_name)
        if rv is not None:
            return rv
        matches = [x for x in self.list_commands(ctx)
                   if x.startswith(cmd_name)]
        if not matches:
            return None
        elif len(matches) == 1:
            return click.Group.get_command(self, ctx, matches[0])
        ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))

The usage output becomes really dumb however: it shows the prefixes of the commands instead of showing them fully:

Usage: test_core a c [OPTIONS]

I would like to see

Usage: test_core add combined [OPTIONS]

even when I call test_core a c -h.

I've looked into it and it doesn't look like there is an obvious solution. Formatter logic doesn't know about their original names. Maybe MultiCommand.resolve_command could be overridden to handle an overridden version of MultiCommand/Group.get_command that returns the original command name as well. But that might break some things, maybe there's some easier way.

Full code:

import click

class AliasedGroup(click.Group):
    def get_command(self, ctx, cmd_name):
        rv = click.Group.get_command(self, ctx, cmd_name)
        if rv is not None:
            return rv
        matches = [x for x in self.list_commands(ctx)
                    if x.startswith(cmd_name)]
        if not matches:
            return None
        elif len(matches) == 1:
            return click.Group.get_command(self, ctx, matches[0])
        ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))

@click.group(cls=AliasedGroup, context_settings={'help_option_names': ['-h', '--help']})
def cli():
    pass

@cli.group(cls=AliasedGroup)
def add():
    pass

@add.command()
@click.option('--yarr')
def combined():
    pass

cli(['a', 'c', '-h'], prog_name='test_core')

Solution

  • You need to keep track of the aliases used.

    The aliases are kept in a global variable because click uses a lot of context instances.

    And you need to implement your own HelpFormatter. This covers all uses of the help construction.

    In the write_usage replace the aliases with the full command names. Keep track of aliases filled to cover the case of test_core a a -h as a command for test_core add auto -h. If an alias is not found in prog don't try the next alias used (while instead of for).

    import click
    
    clickAliases = []
    
    class AliasedGroup(click.Group):
        def get_command(self, ctx, cmd_name):
            rv = click.Group.get_command(self, ctx, cmd_name)
            if rv is not None:
                return rv
            matches = [x for x in self.list_commands(ctx)
                        if x.startswith(cmd_name)]
            if not matches:
                return None
            elif len(matches) == 1:
                clickAliases.append((cmd_name, matches[0]))
                return click.Group.get_command(self, ctx, matches[0])
            ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))
    
    class MyHelpFormatter(click.HelpFormatter):
        def write_usage(self, prog, args="", prefix="Usage: "):
            if clickAliases:
                parts = prog.split()
                partIdx = 0
                for alias,cmd in clickAliases:
                    while partIdx < len(parts):
                        if parts[partIdx] == alias:
                            parts[partIdx] = cmd
                            partIdx += 1
                            break
                        partIdx += 1
                prog = ' '.join(parts)
            click.HelpFormatter.write_usage(self, prog, args, prefix)
    
    def make_formatter(self):
        return MyHelpFormatter(width=self.terminal_width, max_width=self.max_content_width)
    click.Context.make_formatter = make_formatter
    # version 8.x makes if easier with
    # click.Context.formatter_class = MyHelpFormatter
    
    @click.group(cls=AliasedGroup, context_settings={'help_option_names': ['-h', '--help']})
    def cli():
        pass
    
    @cli.group(cls=AliasedGroup)
    def add():
        click.echo("add command")
    
    @add.command()
    @click.option('--yarr')
    def combined(yarr):
        click.echo(f"combined command: {yarr}")
    
    # simulate command arguments - for debugging
    # cli(['a', 'c', '-h'], prog_name='test_core')
    
    # normal start
    cli(prog_name='test_core')
    

    Terminal output

    $ python test_core.py a c -h
    add command
    Usage: test_core add combined [OPTIONS]
    
    Options:
      --yarr TEXT
      -h, --help   Show this message and exit.