Search code examples
pythonpython-3.xcommand-line-interfacepython-click

Click group with options and commands at the same time


I am trying to create application with following behaviour:

myapp - would launch application and do thing A

myapp "some argument" - would do thing B. Thing B is influenced by argument "some argument".

myapp command - would launch "command" (denoted by decorator @cli.command) with feature C. This would be influenced by all the things that click has to offer like @click.option.

Note, that in my application there would me more commands like C.


I've tried to implement this using this code:

import click

class GroupWithOption(click.Group):
    def list_commands(self, ctx):
        return ['command']

    def get_command(self, ctx, cmd_name):
        if cmd_name == 'command':
            return command
        else:
            return do_b


@click.group(cls=GroupWithOption, invoke_without_command=True)
def main():
    print("Does A")

@main.command()
def command():
    print("Does C")

@main.command()
def do_b():
    print("Does B")


if __name__ == '__main__':
    main()

This had mixed results. For one, I can invoke 3 different behaviours (or more) very easily, but I wasn't able to figure out, how to pass argument to the B command. I don't like this solution. It doesn't seem to be a clean one. And to function properly, it would require usage of global variables and some nasty hacks.

Does any of you know about a better way, how to accomplish this?


Solution

  • One way to do that is combine the two answers given here:

    Key Elements:

    @click.group(cls=DefaultCommandGroup, invoke_without_command=True)
    @click.pass_context
    def main(ctx):
        if not ctx.invoked_subcommand:
            click.echo("Does A")
    
    @main.command(default_command=True)
    @click.argument('args', nargs=-1)
    def default_cmd_with_args(args):
        click.echo("Does B: {}".format(args))
    

    How does this work?

    if not ctx.invoked_subcommand:
    

    Allows the group command to be invoked only in the "no command" case, and the

    @click.group(cls=DefaultCommandGroup, invoke_without_command=True)
    

    combined with:

    @main.command(default_command=True)
    

    allows a command to be run if no other command is found.

    Test Code:

    import click
    
    @click.group(cls=DefaultCommandGroup, invoke_without_command=True)
    @click.pass_context
    def main(ctx):
        """My Great CLI"""
        if not ctx.invoked_subcommand:
            click.echo("Does A")
    
    @main.command(default_command=True)
    @click.argument('args', nargs=-1)
    def default_cmd_with_args(args):
        """Command run without a command"""
        click.echo("Does B: {}".format(args))
    
    
    @main.command()
    def cmd_c1():
        """A c1 command"""
        click.echo("Does C1")
    
    
    @main.command()
    def cmd_c2():
        """A c2 command"""
        click.echo("Does C2")
    
    
    if __name__ == "__main__":
        commands = (
            '',
            'random args',
            'cmd_c1',
            'cmd_c2',
            '--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)]
    -----------
    > 
    Does A
    -----------
    > random args
    Does B: ('random', 'args')
    -----------
    > cmd_c1
    Does C1
    -----------
    > cmd_c2
    Does C2
    -----------
    > --help
    Usage: test.py [OPTIONS] COMMAND [ARGS]...
    
      My Great CLI
    
    Options:
      --help  Show this message and exit.
    
    Commands:
      <>      Command run without a command
      cmd_c1  A c1 command
      cmd_c2  A c2 command