A command without name, in Click

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.

Upvotes: 13

Views: 8842

Answers (5)

MalloyDelacroix
MalloyDelacroix

Reputation: 2293

I'm late to the game, but here is a solution based on the answers above but changed to require what I believe is the least amount of additional code to still achieve the default command behavior.

With this method, you are able to specify the default command at the group declaration instead of the command declaration.

class DefaultCommandGroup(click.Group):

def __init__(self, *args, **kwargs):
    self.default_command = kwargs.pop('default_command', None)
    super().__init__(*args, **kwargs)

def resolve_command(self, ctx, args):
    try:
        return super().resolve_command(ctx, args)
    except click.UsageError:
        args.insert(0, self.default_command)
        return super().resolve_command(ctx, args)

Using this class:

# note that the name of the default command is a str here
@click.group(cls=DefaultCommandGroup, default_command='command_one')
def a_group():
    pass

This has the benefit of letting you add command in the usual way.

a_group.add_command(command_one)
a_group.add_command(command_two)
a_group.add_command(command_three)

Upvotes: 3

the_RR
the_RR

Reputation: 392

An end-to-end practical example:

$ test_click add -a 1 -b 2
---
add: 3

------------------
$ test_click sub -a 5 -b 1
---
sub: 4

------------------

$ test_click
enter a operation (add or sub): add
enter a number 1: 1
enter a number 2: 2
---
add: 3

------------------

$ test_click
enter a operation (add or sub): sub
enter a number 1: 5
enter a number 2: 1
---
sub: 4

the code:

import click


@click.group(invoke_without_command=True)
@click.pass_context
def mycommands(ctx):
    if ctx.invoked_subcommand is None:
        manual_mode()
        pass

def manual_mode():
    
    tipo = input('enter a operation (add or sub): ')
    arg1 = input('enter a number 1: ')
    arg2 = input('enter a number 2: ')
    if tipo == 'add':
        add_f(int(arg1), int(arg2))
    elif tipo == 'sub':
        sub_f(int(arg1), int(arg2))
    else:
        print('type not know')


def add_f(arg1,
          arg2):
    print('add:', arg1 + arg2)

def sub_f(arg1,
        arg2):
    
    print('sub:', arg1 - arg2)    


@click.option('-a', 'arg1',
              type=click.INT)
@click.option('-b', 'arg2',
              type=click.INT)
@mycommands.command()
def add(arg1, arg2):
    
    add_f(arg1, arg2)


@click.option('-a', 'arg1',
              type=click.INT)
@click.option('-b', 'arg2',
              type=click.INT)
@mycommands.command()
def sub(arg1,
        arg2):
    
    sub_f(arg1, arg2)


if __name__ == '__main__':
    mycommands()

Upvotes: 0

edobez
edobez

Reputation: 115

click-default-group is doing what you are looking for. It is part of the click-contrib collection.

The advantage of that instead of using invoke_without_command is that it passes the options and arguments flawlessly to the default command, something that is not trivial (or even possible) with the built-in functionality.

Example code:

import click
from click_default_group import DefaultGroup

@click.group(cls=DefaultGroup, default='foo', default_if_no_args=True)
def cli():
    print("group execution")

@cli.command()
@click.option('--config', default=None)
def foo(config):
    click.echo('foo execution')
    if config:
        click.echo(config)

Then, it's possible to call foo command with its option as:

$ program foo --config bar     <-- normal way to call foo
$ program --config bar         <-- foo is called and the option is forwarded. 
                                   Not possible with vanilla Click.

Upvotes: 10

Stephen Rauch
Stephen Rauch

Reputation: 49794

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

Upvotes: 6

georgexsh
georgexsh

Reputation: 16624

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')

Upvotes: 11

Related Questions