0

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')
2
  • what is your full example program that has this behavior Commented Nov 5, 2020 at 10:01
  • @rioV8 Yeah, I should have done that, now it's there. Commented Nov 5, 2020 at 14:44

1 Answer 1

1

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

6 Comments

Usage: test_core add combined [OPTIONS] != Usage: test_core.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... -- Additionally, command chaining is not my interest. Group hierarchy with command leaves is.
Output to an invocation with cli(['a', 'c', '-h'], prog_name='test_core') is this actually: Usage: test_core c [OPTIONS] -- equally bad.
@MatrixTheatrics You have to write your own HelpFormatter. See edit
Nice job, thanks! I modified it a bit for myself so that the aliases list is stored in the current context's dict obj. I already have these classes/functions overloaded, so it's easily inserted into my code.
@MatrixTheatrics In my test I found a new context for every argument, or at least a new value for ctx.command_path, maybe it was for the same context
|

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.