A command without name, in Click

Question:

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.

Asked By: bcusk07q

||

Answers:

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')
Answered By: georgexsh

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
Answered By: Stephen Rauch

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.

Answered By: edobez

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()
Answered By: the_RR
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.