A command without name, in Click

半城伤御伤魂 提交于 2020-01-14 10:38:10

问题


I want to have a command line tool with a usage like this:

$ program <arg>            does something, no command name required
$ program cut <arg>
$ program eat <arg>

The Click code would look like this:

@click.group()
def main() :
    pass

@main.command()
@click.argument('arg')
def noname(arg) :
    # does stuff

@main.command()
@click.argument('arg')
def cut(arg) :
    # cuts stuff

@main.command()
@click.argument('arg')
def eat(arg) :
    # eats stuff

My problem is that with this code, there is always a required command name, ie: I need to run $ program noname arg. But I want to be able to run $ program arg.


回答1:


Your scheme has some challenges because of the ambiguity introduce with the default command. Regardless, here is one way that can be achieved with click. As shown in the test results, the generated help with be less than ideal, but likely OK.

Custom Class:

import click

class DefaultCommandGroup(click.Group):
    """allow a default command for a group"""

    def command(self, *args, **kwargs):
        default_command = kwargs.pop('default_command', False)
        if default_command and not args:
            kwargs['name'] = kwargs.get('name', '<>')
        decorator = super(
            DefaultCommandGroup, self).command(*args, **kwargs)

        if default_command:
            def new_decorator(f):
                cmd = decorator(f)
                self.default_command = cmd.name
                return cmd

            return new_decorator

        return decorator

    def resolve_command(self, ctx, args):
        try:
            # test if the command parses
            return super(
                DefaultCommandGroup, self).resolve_command(ctx, args)
        except click.UsageError:
            # command did not parse, assume it is the default command
            args.insert(0, self.default_command)
            return super(
                DefaultCommandGroup, self).resolve_command(ctx, args)

Using the Custom Class

To use the custom class, pass the cls parameter to the click.group() decorator. Then pass default_command=True for the command which will be the default.

@click.group(cls=DefaultCommandGroup)
def a_group():
    """My Amazing Group"""

@a_group.command(default_command=True)
def a_command():
    """a command under the group"""

How does this work?

This works because click is a well designed OO framework. The @click.group() decorator usually instantiates a click.Group object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Group in our own class and over ride desired methods.

In this case we over ride click.Group.command() so that when a command is added we find the default command. In addition we override click.Group.resolve_command() so that we can insert the default command name if the first resolution is unsuccessful.

Test Code:

@click.group(cls=DefaultCommandGroup)
def main():
    pass

@main.command(default_command=True)
@click.argument('arg')
def noname(arg):
    """ does stuff """
    click.echo('default: {}'.format(arg))

@main.command()
@click.argument('arg')
def cut(arg):
    """ cuts stuff """
    click.echo('cut: {}'.format(arg))

@main.command()
@click.argument('arg')
def eat(arg):
    """ eats stuff """
    click.echo('eat: {}'.format(arg))


if __name__ == "__main__":
    commands = (
        'an_arg',
        'cut cut_arg',
        'eat eat_arg',
        '--help',
        'cut --help',
        'eat --help',
        '',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for command in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + command)
            time.sleep(0.1)
            main(command.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc,
                                   (click.ClickException, SystemExit)):
                raise

Results:

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> an_arg
default: an_arg
-----------
> cut cut_arg
cut: cut_arg
-----------
> eat eat_arg
eat: eat_arg
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  <>   does stuff
  cut  cuts stuff
  eat  eats stuff
-----------
> cut --help
Usage: test.py cut [OPTIONS] ARG

  cuts stuff

Options:
  --help  Show this message and exit.
-----------
> eat --help
Usage: test.py eat [OPTIONS] ARG

  eats stuff

Options:
  --help  Show this message and exit.
-----------
> 
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  <>   does stuff
  cut  cuts stuff
  eat  eats stuff



回答2:


There is an option for you, "Group Invocation Without Command ":

@click.group(invoke_without_command=True)
@click.pass_context
def main(ctx):
    if not ctx.invoked_subcommand:
        print('main stuff')


来源:https://stackoverflow.com/questions/52053491/a-command-without-name-in-click

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!