Reputation: 379
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
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
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
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
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.
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)
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"""
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.
@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
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
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