Is there an equivalent to argparse's nargs='*' functionality for optional arguments in Click?
I am writing a command line script, and one of the options needs to be able to take an unlimited number of arguments, like:
foo --users alice bob charlie --bar baz
So users would be ['alice', 'bob', 'charlie'] and bar would be 'baz'.
In argparse, I can specify multiple optional arguments to collect all of the arguments that follow them by setting nargs='*'.
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--users', nargs='*')
>>> parser.add_argument('--bar')
>>> parser.parse_args('--users alice bob charlie --bar baz'.split())
Namespace(bar='baz', users=['alice', 'bob', 'charlie'])
I know Click allows you to specify an argument to accept unlimited inputs by setting nargs=-1, but when I try to set an optional argument's nargs to -1, I get:
TypeError: Options cannot have nargs < 0
Is there a way to make Click accept an unspecified number of arguments for an option?
Update:
I need to be able to specify options after the option that takes unlimited arguments.
Update:
#Stephen Rauch's answer answers this question. However, I don't recommend using the approach I ask for here. My feature request is intentionally not implemented in Click, since it can result in unexpected behaviors. Click's recommended approach is to use multiple=True:
#click.option('-u', '--user', 'users', multiple=True)
And in the command line, it will look like:
foo -u alice -u bob -u charlie --bar baz
One way to approach what you are after is to inherit from click.Option, and customize the parser.
Custom Class:
import click
class OptionEatAll(click.Option):
def __init__(self, *args, **kwargs):
self.save_other_options = kwargs.pop('save_other_options', True)
nargs = kwargs.pop('nargs', -1)
assert nargs == -1, 'nargs, if set, must be -1 not {}'.format(nargs)
super(OptionEatAll, self).__init__(*args, **kwargs)
self._previous_parser_process = None
self._eat_all_parser = None
def add_to_parser(self, parser, ctx):
def parser_process(value, state):
# method to hook to the parser.process
done = False
value = [value]
if self.save_other_options:
# grab everything up to the next option
while state.rargs and not done:
for prefix in self._eat_all_parser.prefixes:
if state.rargs[0].startswith(prefix):
done = True
if not done:
value.append(state.rargs.pop(0))
else:
# grab everything remaining
value += state.rargs
state.rargs[:] = []
value = tuple(value)
# call the actual process
self._previous_parser_process(value, state)
retval = super(OptionEatAll, self).add_to_parser(parser, ctx)
for name in self.opts:
our_parser = parser._long_opt.get(name) or parser._short_opt.get(name)
if our_parser:
self._eat_all_parser = our_parser
self._previous_parser_process = our_parser.process
our_parser.process = parser_process
break
return retval
Using Custom Class:
To use the custom class, pass the cls parameter to #click.option() decorator like:
#click.option("--an_option", cls=OptionEatAll)
or if it is desired that the option will eat the entire rest of the command line, not respecting other options:
#click.option("--an_option", cls=OptionEatAll, save_other_options=False)
How does this work?
This works because click is a well designed OO framework. The #click.option() decorator usually instantiates a
click.Option object but allows this behavior to be over ridden with the cls parameter. So it is a relatively
easy matter to inherit from click.Option in our own class and over ride the desired methods.
In this case we over ride click.Option.add_to_parser() and the monkey patch the parser so that we can
eat more than one token if desired.
Test Code:
#click.command()
#click.option('-g', 'greedy', cls=OptionEatAll, save_other_options=False)
#click.option('--polite', cls=OptionEatAll)
#click.option('--other')
def foo(polite, greedy, other):
click.echo('greedy: {}'.format(greedy))
click.echo('polite: {}'.format(polite))
click.echo('other: {}'.format(other))
if __name__ == "__main__":
commands = (
'-g a b --polite x',
'-g a --polite x y --other o',
'--polite x y --other o',
'--polite x -g a b c --other o',
'--polite x --other o -g a b c',
'-g a b c',
'-g a',
'-g',
'extra',
'--help',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + cmd)
time.sleep(0.1)
foo(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Test 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)]
-----------
> -g a b --polite x
greedy: ('a', 'b', '--polite', 'x')
polite: None
other: None
-----------
> -g a --polite x y --other o
greedy: ('a', '--polite', 'x', 'y', '--other', 'o')
polite: None
other: None
-----------
> --polite x y --other o
greedy: None
polite: ('x', 'y')
other: o
-----------
> --polite x -g a b c --other o
greedy: ('a', 'b', 'c', '--other', 'o')
polite: ('x',)
other: None
-----------
> --polite x --other o -g a b c
greedy: ('a', 'b', 'c')
polite: ('x',)
other: o
-----------
> -g a b c
greedy: ('a', 'b', 'c')
polite: None
other: None
-----------
> -g a
greedy: ('a',)
polite: None
other: None
-----------
> -g
Error: -g option requires an argument
-----------
> extra
Usage: test.py [OPTIONS]
Error: Got unexpected extra argument (extra)
-----------
> --help
Usage: test.py [OPTIONS]
Options:
-g TEXT
--polite TEXT
--other TEXT
--help Show this message and exit.
You can use this trick.
import click
#click.command()
#click.option('--users', nargs=0, required=True)
#click.argument('users', nargs=-1)
#click.option('--bar')
def fancy_command(users, bar):
users_str = ', '.join(users)
print('Users: {}. Bar: {}'.format(users_str, bar))
if __name__ == '__main__':
fancy_command()
Add fake option with a needed name and none arguments nargs=0, then add 'argument' with the unlimited args nargs=-1.
$ python foo --users alice bob charlie --bar baz
Users: alice, bob, charlie. Bar: baz
But be careful with the further options:
$ python foo --users alice bob charlie --bar baz faz
Users: alice, bob, charlie, faz. Bar: baz
I ran into the same issue. Instead of implementing a single command line option with n number of arguments, I decided to use multiple of the same command line option and just letting Click make a tuple out of the arguments under the hood. I ultimately figured if Click didn't support it, that decision was probably made for a good reason.
https://click.palletsprojects.com/en/7.x/options/#multiple-options
here is an example of what I am saying:
instead of passing a single string argument a splitting on a delimiter:
commit -m foo:bar:baz
I opted to use this:
commit -m foo -m bar -m baz
here is the source code:
#click.command()
#click.option('--message', '-m', multiple=True)
def commit(message):
click.echo('\n'.join(message))
This is more to type, but I do think it makes the CLI more user friendly and robust.
I needed this for myself and thought of settling for the solution provided by #nikita-malovichko even though it is very restrictive, but it didn't work for me (see my comment to that answer) so came up with the below alternative.
My solution doesn't directly address the question on how to support nargs=*, but it provided a good alternative for myself so sharing it for the benefit of others.
The idea is to use one option that specifies the expected count for another, i.e., set the nargs count dynamically at runtime. Here is a quick demo:
import click
def with_dynamic_narg(cnt_opt, tgt_opt):
class DynamicNArgSetter(click.Command):
def parse_args(self, ctx, args):
ctx.resilient_parsing = True
parser = self.make_parser(ctx)
opts, _, _ = parser.parse_args(args=list(args))
if cnt_opt in opts:
for p in self.params:
if isinstance(p, click.Option) and p.name == tgt_opt:
p.nargs = int(opts[cnt_opt])
ctx.resilient_parsing = False
return super().parse_args(ctx, args)
return DynamicNArgSetter
#click.command(cls=with_dynamic_narg('c', 'n'))
#click.option("-c", type=click.INT)
#click.option("-n", nargs=0)
def f(c, n):
print(c, n)
if __name__ == '__main__':
f()
In the above code, a custom Command class is created that knows the link between the "count" arg and the target arg that takes multiple args. It first does a local parsing in "resilient" mode to detect the count, then uses the count to update the nargs value of the target arg and then resumes parsing in normal mode.
Here is some sample interaction:
$ python t.py -c 0
0 None
$ python t.py -c 1
Usage: t.py [OPTIONS]
Try 't.py --help' for help.
Error: Missing option '-n'.
$ python t.py -c 0 -n a
Usage: t.py [OPTIONS]
Try 't.py --help' for help.
Error: Got unexpected extra argument (a)
$ python t.py -c 1 -n a
1 a
$ python /tmp/t.py -c 2 -n a b
2 ('a', 'b')
Note: The advantage over the official recommendation of using multiple=True is that we can use filename wildcards and let shell expand them. E.g.,
$ touch abc.1 abc.2
$ python t.py -c 2 -n abc.*
2 ('abc.1', 'abc.2')
$ python t.py -c $(echo abc.* | wc -w) -n abc.*
2 ('abc.1', 'abc.2')
Related
How can I achieve the following synopsis using the Python click library?
Usage: app CMD [OPTIONS] [FOO] [BAR]
app CMD [OPTIONS] [FOOBAR]
I can't figure out whether I am able to pass two different sets of named argument for the same command based on the number of given arguments. That is, if only one argument was passed it's foobar, but if two arguments were passed, they are foo and bar.
The code representation of such implementation would look something like this (provided you could use function overload, which you can't)
#click.command()
#click.argument('foo', required=False)
#click.argument('bar', required=False)
def cmd(foo, bar):
# ...
#click.command()
#click.argument('foobar', required=False)
def cmd(foobar):
# ...
You can add multiple command handlers with a different number of arguments for each by creating a custom click.Command class. There is some ambiguity around which of the command handlers would best be called if parameters are not strictly required, but that can be mostly dealt with by using the first signature that fits the command line passed.
Custom Class
class AlternateArgListCmd(click.Command):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.alternate_arglist_handlers = [(self, super())]
self.alternate_self = self
def alternate_arglist(self, *args, **kwargs):
from click.decorators import command as cmd_decorator
def decorator(f):
command = cmd_decorator(*args, **kwargs)(f)
self.alternate_arglist_handlers.append((command, command))
# verify we have no options defined and then copy options from base command
options = [o for o in command.params if isinstance(o, click.Option)]
if options:
raise click.ClickException(
f'Options not allowed on {type(self).__name__}: {[o.name for o in options]}')
command.params.extend(o for o in self.params if isinstance(o, click.Option))
return command
return decorator
def make_context(self, info_name, args, parent=None, **extra):
"""Attempt to build a context for each variant, use the first that succeeds"""
orig_args = list(args)
for handler, handler_super in self.alternate_arglist_handlers:
args[:] = list(orig_args)
self.alternate_self = handler
try:
return handler_super.make_context(info_name, args, parent, **extra)
except click.UsageError:
pass
except:
raise
# if all alternates fail, return the error message for the first command defined
args[:] = orig_args
return super().make_context(info_name, args, parent, **extra)
def invoke(self, ctx):
"""Use the callback for the appropriate variant"""
if self.alternate_self.callback is not None:
return ctx.invoke(self.alternate_self.callback, **ctx.params)
return super().invoke(ctx)
def format_usage(self, ctx, formatter):
"""Build a Usage for each variant"""
prefix = "Usage: "
for _, handler_super in self.alternate_arglist_handlers:
pieces = handler_super.collect_usage_pieces(ctx)
formatter.write_usage(ctx.command_path, " ".join(pieces), prefix=prefix)
prefix = " " * len(prefix)
Using the Custom Class:
To use the custom class, pass it as the cls argument to the click.command decorator like:
#click.command(cls=AlternateArgListCmd)
#click.argument('foo')
#click.argument('bar')
def cli(foo, bar):
...
Then use the alternate_arglist() decorator on the command to add another
command handler with different arguments.
#cli.alternate_arglist()
#click.argument('foobar')
def cli_one_param(foobar):
...
How does this work?
This works because click is a well designed OO framework. The #click.command() decorator
usually instantiates a click.Command object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Command in our own class and over ride the desired methods.
In this case we add a new decorator method: alternate_arglist(), and override three methods: make_context(), invoke() & format_usage(). The overridden make_context() method checks to see which of the command handler variants matches the number of args passed, the overridden invoke() method is used to call the appropriate command handler variant and the overridden format_usage() is used to create the help message showing the various usages.
Test Code:
import click
#click.command(cls=AlternateArgListCmd)
#click.argument('foo')
#click.argument('bar')
#click.argument('baz')
#click.argument('bing', required=False)
#click.option('--an-option', default='empty')
def cli(foo, bar, baz, bing, an_option):
"""Best Command Ever!"""
if bing is not None:
click.echo(f'foo bar baz bing an-option: {foo} {bar} {baz} {bing} {an_option}')
else:
click.echo(f'foo bar baz an-option: {foo} {bar} {baz} {an_option}')
#cli.alternate_arglist()
#click.argument('foo')
#click.argument('bar')
def cli_two_param(foo, bar, an_option):
click.echo(f'foo bar an-option: {foo} {bar} {an_option}')
#cli.alternate_arglist()
#click.argument('foobar', required=False)
def cli_one_param(foobar, an_option):
click.echo(f'foobar an-option: {foobar} {an_option}')
if __name__ == "__main__":
commands = (
'',
'p1',
'p1 p2 --an-option=optional',
'p1 p2 p3',
'p1 p2 p3 p4 --an-option=optional',
'p1 p2 p3 p4 p5',
'--help',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + cmd)
time.sleep(0.1)
cli(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Test Results:
Click Version: 7.1.2
Python Version: 3.8.5 (tags/v3.8.5:580fbb0, Jul 20 2020, 15:57:54) [MSC v.1924 64 bit (AMD64)]
-----------
>
foobar an-option: None empty
-----------
> p1
foobar an-option: p1 empty
-----------
> p1 p2 --an-option=optional
foo bar an-option: p1 p2 optional
-----------
> p1 p2 p3
foo bar baz an-option: p1 p2 p3 empty
-----------
> p1 p2 p3 p4 --an-option=optional
foo bar baz bing an-option: p1 p2 p3 p4 optional
-----------
> p1 p2 p3 p4 p5
Usage: test_code.py [OPTIONS] FOO BAR BAZ [BING]
test_code.py [OPTIONS] FOO BAR
test_code.py [OPTIONS] [FOOBAR]
Try 'test_code.py --help' for help.
Error: Got unexpected extra argument (p5)
-----------
> --help
Usage: test_code.py [OPTIONS] FOO BAR BAZ [BING]
test_code.py [OPTIONS] FOO BAR
test_code.py [OPTIONS] [FOOBAR]
Best Command Ever!
Options:
--an-option TEXT
--help Show this message and exit.
I need the script to look into the arguments given in command line and give an error output if two specific arguments are given in the same command line.
Please note that parameters b & c are mutually exclusive.
I need to have a way that if in the command line both -b & -c is given, the system will provide an error message and exit. Also if there any other way to write the code?
Thanks, NH
My sample code is like this:
import getopt
def main():
x = ''
try:
opts, args = getopt.getopt(sys.argv[1:], "habc",["help","Task_a", "Task_b", "Task_c"])
except getopt.GetoptError:
print("Wrong Parameter")
sys.exit()
for opt, args in opts:
if opt in ("-h", "--help"):
x = "h"
elif opt in ("-a", "--Task_a"):
x= "a"
elif opt in ("-b", "--Task_b"):
x = "b"
elif opt in ("-c", "--Task_c"):
x = "c"
else:
x = "something Else"
return x
if __name__ =="main":
main()
print(main())
First of all, you should use argparse module that support mutual exclusion.
To answer your question, you could use this simple logic
optnames = [opt[0] for opt in opts]
if (("-b" in optnames or "--Task-b" in optnames) and
("-c" in optnames or "--Task-c" in optnames)):
print("-b and -c are mutually exclusive", file=sys.stderr)
sys.exit()
Use argparse for that.
Here's a simple example to make it work:
parser = argparse.ArgumentParser(description='Doing some tasks')
parser.add_argument('-b', action='store_true', help="Proceed to task B")
parser.add_argument('-c', action='store_true', help="Proceed to task C")
args = parser.parse_args('-b -c'.split())
if args.b and args.c:
sys.exit()
if args.b:
# do something
if args.c:
# do something else
EDIT:
You can also use a mutually exclusive group. Thanks for suggesting shiplu.
parser = argparse.ArgumentParser(description='Doing some tasks')
group = parser.add_mutually_exclusive_group()
group.add_argument('-b', action='store_true', help="Proceed to task B")
group.add_argument('-c', action='store_true', help="Proceed to task C")
And then when you try to enter both of the arguments:
In [80]: args=parser.parse_args('-b -c'.split())
usage: ipython [-h] [-b | -c]
ipython: error: argument -c: not allowed with argument -b
An exception has occurred, use %tb to see the full traceback.
SystemExit: 2
Otherwise,
In [82]: parser.parse_args('-b'.split())
Out[82]: Namespace(b=True, c=False)
In [83]: parser.parse_args('-c'.split())
Out[83]: Namespace(b=False, c=True)
suppose I have the module myscript.py; This module is production code, and is called often as %dir%>python myscript.py foo bar.
I want to extend it to take keyword arguments. I know that I can take these arguments using the script below, but unfortunately one would have to call it using
%dir%>python myscript.py main(foo, bar).
I know that I can use the argparse module, but I'm not sure how to do it.
import sys
def main(foo,bar,**kwargs):
print 'Called myscript with:'
print 'foo = %s' % foo
print 'bar = %s' % bar
if kwargs:
for k in kwargs.keys():
print 'keyword argument : %s' % k + ' = ' + '%s' % kwargs[k]
if __name__=="__main__":
exec(''.join(sys.argv[1:]))
#Moon beat me to it with a similar solution, but I'd suggest doing the parsing beforehand and passing in actual kwargs:
import sys
def main(foo, bar, **kwargs):
print('Called myscript with:')
print('foo = {}'.format(foo))
print('bar = {}'.format(bar))
for k, v in kwargs.items():
print('keyword argument: {} = {}'.format(k, v))
if __name__=='__main__':
main(sys.argv[1], # foo
sys.argv[2], # bar
**dict(arg.split('=') for arg in sys.argv[3:])) # kwargs
# Example use:
# $ python myscript.py foo bar hello=world 'with spaces'='a value'
# Called myscript with:
# foo = foo
# bar = bar
# keyword argument: hello = world
# keyword argument: with spaces = a value
First, you won't be passing an arbitrary Python expression as an argument. It's brittle and unsafe.
To set up the argument parser, you define the arguments you want, then parse them to produce a Namespace object that contains the information specified by the command line call.
import argparse
p = argparse.ArgumentParser()
p.add_argument('foo')
p.add_argument('bar')
p.add_argument('--add-feature-a', dest='a', action='store_true', default=False)
In your __main__ block, you'll parse the arguments, then pass a dictionary produced from the Namespace to main.
if __name__ == '__main__':
args = p.parse_args()
main(**vars(args))
Then you'll call your script with a line like
# foo = "3", bar = "6", a = True
python myscript.py 3 6 --add-feature-a
or
# foo = "moo", bar="7.7", a = False
python myscript.py moo 7.7
There's a lot more you can do with argparse, but this is a simple example for getting the value it produces into main.
If you want to pass in keyword arguments as you would in the main function, key=value, you can do it like so:
import sys
def main(foo, bar, *args):
print "Called my script with"
print "foo = %s" % foo
print "bar = %s" % bar
for arg in args:
k = arg.split("=")[0]
v = arg.split("=")[1]
print "Keyword argument: %s = %s" % (k, v)
if __name__ == "__main__":
if len(sys.argv) < 3:
raise SyntaxError("Insufficient arguments.")
if len(sys.argv) != 3:
# If there are keyword arguments
main(sys.argv[1], sys.argv[2], *sys.argv[3:])
else:
# If there are no keyword arguments
main(sys.argv[1], sys.argv[2])
Some examples:
$> python my_file.py a b x=4
Called my script with
foo = a
bar = b
Keyword argument: x = 4
$> python my_file.py foo bar key=value
Called my script with
foo = foo
bar = bar
Keyword argument: key = value
However, this assumes that the key and value do not have any whitespace between them, key = value will not work.
If you are looking for --argument kinds of keyword arguments, you should use argparse.
in two lines of code I can get args and kwargs that I can manipulate like standard args and kwargs:
import sys
if __name__=='__main__':
argv=argv[1:]
kwargs={kw[0]:kw[1] for kw in [ar.split('=') for ar in argv if ar.find('=')>0]}
args=[arg for arg in argv if arg.find('=')<0]
#and you can the use args and kwargs as so:
if 'reset' in args:
do_some_functions_with_reset()
a_device_var=kwargs.get('device',False):
#or whatever you want to do with args and kwargs
and the result is :
$python test.py reset device=foo format=1 bar
->args=['reset','bar']
->kwargs={'device':'foo','format':'1'}
With a bit of introspection, it's possible to set up ArgumentParser from a function's signature, thus mapping command-line parameters directly to function arguments:
import argparse
import inspect
def myfun(mode, count=1, frobify=False, *files):
print('Doing %s %d times on %s (%sfrobifying)' % (
mode, count, files, '' if frobify else 'not '
))
def funopt(fun, argv=None):
parser = argparse.ArgumentParser()
if hasattr(inspect, 'getfullargspec'):
spec = inspect.getfullargspec(fun)
else:
spec = inspect.getargspec(fun)
num_defaults = len(spec.defaults) if spec.defaults is not None else 0
for i in range(len(spec.args)):
if i < len(spec.args) - num_defaults:
parser.add_argument(spec.args[i])
elif spec.defaults[i - len(spec.args)] is False:
parser.add_argument('--' + spec.args[i],
default=False, action='store_true')
else:
default = spec.defaults[i - len(spec.args)]
parser.add_argument('--' + spec.args[i],
default=default,
type=type(default))
if spec.varargs is not None:
parser.add_argument(spec.varargs,
nargs='*')
kwargs = vars(parser.parse_args(argv))
args = []
for arg in spec.args:
args += [kwargs[arg]]
if spec.varargs is not None:
args += kwargs[spec.varargs]
fun(*args)
funopt(myfun)
The result:
$ python test.py
usage: test.py [-h] [--count COUNT] [--frobify] mode [files [files ...]]
test.py: error: too few arguments
$ python test.py myaction a b c
Doing myaction 1 times on ('a', 'b', 'c') (not frobifying)
$ python test.py --frobify --count=5 myaction a b c
Doing myaction 5 times on ('a', 'b', 'c') (frobifying)
I'm writing a relatively simple Python script which supports a couple of different commands. The different commands support different options and I want to be able to pass the options parsed by argparse to the correct method for the specified command.
The usage string looks like so:
usage: script.py [-h]
{a, b, c}
...
script.py: error: too few arguments
I can easily call the appropriate method:
def a():
...
def b():
...
def c():
...
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.set_defaults(method = a)
...
arguments = parser.parse_args()
arguments.method()
However, I have to pass arguments to these methods and they all accept different sets of arguments.
Currently, I just pass the Namespace object returned by argparse, like so:
def a(arguments):
arg1 = getattr(arguments, 'arg1', None)
...
This seems a little awkward, and makes the methods a little harder to reuse as I have to pass arguments as a dict or namespace rather than as usual parameters.
I would like someway of defining the methods with parameters (as you would a normal function) and still be able to call them dynamically while passing appropriate parameters. Like so:
def a(arg1, arg2):
...
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.set_defaults(method = a)
...
arguments = parser.parse_args()
arguments.method() # <<<< Arguments passed here somehow
Any ideas?
I found quite a nice solution:
import argparse
def a(arg1, arg2, **kwargs):
print arg1
print arg2
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.set_defaults(method = a)
parser.add_argument('arg1', type = str)
parser.add_argument('arg2', type = str)
arguments = parser.parse_args()
arguments.method(**vars(arguments))
Of course there's a minor problem if the arguments of the method clash with the names of the arguments argparse uses, though I think this is preferable to passing the Namespace object around and using getattr.
You're probably trying to achieve the functionality that sub-commands provide:
http://docs.python.org/dev/library/argparse.html#sub-commands
Not sure how practical this is, but by using inspect you can leave out the extraneous **kwargs parameter on your functions, like so:
import argparse
import inspect
def sleep(seconds=0):
print "sleeping", seconds, "seconds"
def foo(a, b=2, **kwargs):
print "a=",a
print "b=",b
print "kwargs=",kwargs
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title="subcommand")
parser_sleep = subparsers.add_parser('sleep')
parser_sleep.add_argument("seconds", type=int, default=0)
parser_sleep.set_defaults(func=sleep)
parser_foo = subparsers.add_parser('foo')
parser_foo.add_argument("-a", type=int, default=101)
parser_foo.add_argument("-b", type=int, default=201)
parser_foo.add_argument("--wacky", default=argparse.SUPPRESS)
parser_foo.set_defaults(func=foo)
args = parser.parse_args()
arg_spec = inspect.getargspec(args.func)
if arg_spec.keywords:
## convert args to a dictionary
args_for_func = vars(args)
else:
## get a subset of the dictionary containing just the arguments of func
args_for_func = {k:getattr(args, k) for k in arg_spec.args}
args.func(**args_for_func)
Examples:
$ python test.py sleep 23
sleeping 23 seconds
$ python test.py foo -a 333 -b 444
a= 333
b= 444
kwargs= {'func': <function foo at 0x10993dd70>}
$ python test.py foo -a 333 -b 444 --wacky "this is wacky"
a= 333
b= 444
kwargs= {'func': <function foo at 0x10a321d70>, 'wacky': 'this is wacky'}
Have fun!
How can I have a default sub-command, or handle the case where no sub-command is given using argparse?
import argparse
a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')
a.parse_args()
Here I'd like a command to be selected, or the arguments to be handled based only on the next highest level of parser (in this case the top-level parser).
joiner#X:~/src> python3 default_subcommand.py
usage: default_subcommand.py [-h] {hi} ...
default_subcommand.py: error: too few arguments
On Python 3.2 (and 2.7) you will get that error, but not on 3.3 and 3.4 (no response). Therefore on 3.3/3.4 you could test for parsed_args to be an empty Namespace.
A more general solution is to add a method set_default_subparser() (taken from the ruamel.std.argparse package) and call that method just before parse_args():
import argparse
import sys
def set_default_subparser(self, name, args=None, positional_args=0):
"""default subparser selection. Call after setup, just before parse_args()
name: is the name of the subparser to call by default
args: if set is the argument list handed to parse_args()
, tested with 2.7, 3.2, 3.3, 3.4
it works with 2.6 assuming argparse is installed
"""
subparser_found = False
for arg in sys.argv[1:]:
if arg in ['-h', '--help']: # global help if no subparser
break
else:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map.keys():
if sp_name in sys.argv[1:]:
subparser_found = True
if not subparser_found:
# insert default in last position before global positional
# arguments, this implies no global options are specified after
# first positional argument
if args is None:
sys.argv.insert(len(sys.argv) - positional_args, name)
else:
args.insert(len(args) - positional_args, name)
argparse.ArgumentParser.set_default_subparser = set_default_subparser
def do_hi():
print('inside hi')
a = argparse.ArgumentParser()
b = a.add_subparsers()
sp = b.add_parser('hi')
sp.set_defaults(func=do_hi)
a.set_default_subparser('hi')
parsed_args = a.parse_args()
if hasattr(parsed_args, 'func'):
parsed_args.func()
This will work with 2.6 (if argparse is installed from PyPI), 2.7, 3.2, 3.3, 3.4. And allows you to do both
python3 default_subcommand.py
and
python3 default_subcommand.py hi
with the same effect.
Allowing to chose a new subparser for default, instead of one of the existing ones.
The first version of the code allows setting one of the previously-defined subparsers as a default one. The following modification allows adding a new default subparser, which could then be used to specifically process the case when no subparser was selected by user (different lines marked in the code)
def set_default_subparser(self, name, args=None, positional_args=0):
"""default subparser selection. Call after setup, just before parse_args()
name: is the name of the subparser to call by default
args: if set is the argument list handed to parse_args()
, tested with 2.7, 3.2, 3.3, 3.4
it works with 2.6 assuming argparse is installed
"""
subparser_found = False
existing_default = False # check if default parser previously defined
for arg in sys.argv[1:]:
if arg in ['-h', '--help']: # global help if no subparser
break
else:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map.keys():
if sp_name in sys.argv[1:]:
subparser_found = True
if sp_name == name: # check existance of default parser
existing_default = True
if not subparser_found:
# If the default subparser is not among the existing ones,
# create a new parser.
# As this is called just before 'parse_args', the default
# parser created here will not pollute the help output.
if not existing_default:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
x.add_parser(name)
break # this works OK, but should I check further?
# insert default in last position before global positional
# arguments, this implies no global options are specified after
# first positional argument
if args is None:
sys.argv.insert(len(sys.argv) - positional_args, name)
else:
args.insert(len(args) - positional_args, name)
argparse.ArgumentParser.set_default_subparser = set_default_subparser
a = argparse.ArgumentParser()
b = a.add_subparsers(dest ='cmd')
sp = b.add_parser('hi')
sp2 = b.add_parser('hai')
a.set_default_subparser('hey')
parsed_args = a.parse_args()
print(parsed_args)
The "default" option will still not show up in the help:
python test_parser.py -h
usage: test_parser.py [-h] {hi,hai} ...
positional arguments:
{hi,hai}
optional arguments:
-h, --help show this help message and exit
However, it is now possible to differentiate between and separately handle calling one of the provided subparsers, and calling the default subparser when no argument was provided:
$ python test_parser.py hi
Namespace(cmd='hi')
$ python test_parser.py
Namespace(cmd='hey')
It seems I've stumbled on the solution eventually myself.
If the command is optional, then this makes the command an option. In my original parser configuration, I had a package command that could take a range of possible steps, or it would perform all steps if none was given. This makes the step a choice:
parser = argparse.ArgumentParser()
command_parser = subparsers.add_parser('command')
command_parser.add_argument('--step', choices=['prepare', 'configure', 'compile', 'stage', 'package'])
...other command parsers
parsed_args = parser.parse_args()
if parsed_args.step is None:
do all the steps...
Here's a nicer way of adding a set_default_subparser method:
class DefaultSubcommandArgParse(argparse.ArgumentParser):
__default_subparser = None
def set_default_subparser(self, name):
self.__default_subparser = name
def _parse_known_args(self, arg_strings, *args, **kwargs):
in_args = set(arg_strings)
d_sp = self.__default_subparser
if d_sp is not None and not {'-h', '--help'}.intersection(in_args):
for x in self._subparsers._actions:
subparser_found = (
isinstance(x, argparse._SubParsersAction) and
in_args.intersection(x._name_parser_map.keys())
)
if subparser_found:
break
else:
# insert default in first position, this implies no
# global options without a sub_parsers specified
arg_strings = [d_sp] + arg_strings
return super(DefaultSubcommandArgParse, self)._parse_known_args(
arg_strings, *args, **kwargs
)
Maybe what you're looking for is the dest argument of add_subparsers:
(Warning: works in Python 3.4, but not in 2.7)
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='cmd')
parser_hi = subparsers.add_parser('hi')
parser.parse_args([]) # Namespace(cmd=None)
Now you can just use the value of cmd:
if cmd in [None, 'hi']:
print('command "hi"')
You can duplicate the default action of a specific subparser on the main parser, effectively making it the default.
import argparse
p = argparse.ArgumentParser()
sp = p.add_subparsers()
a = sp.add_parser('a')
a.set_defaults(func=do_a)
b = sp.add_parser('b')
b.set_defaults(func=do_b)
p.set_defaults(func=do_b)
args = p.parse_args()
if args.func:
args.func()
else:
parser.print_help()
Does not work with add_subparsers(required=True), which is why the if args.func is down there.
In my case I found it easiest to explicitly provide the subcommand name to parse_args() when argv was empty.
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='commands')
runParser = subparsers.add_parser('run', help='[DEFAULT ACTION]')
altParser = subparsers.add_parser('alt', help='Alternate command')
altParser.add_argument('alt_val', type=str, help='Value required for alt command.')
# Here's my shortcut: If `argv` only contains the script name,
# manually inject our "default" command.
args = parser.parse_args(['run'] if len(sys.argv) == 1 else None)
print args
Example runs:
$ ./test.py
Namespace()
$ ./test.py alt blah
Namespace(alt_val='blah')
$ ./test.py blah
usage: test.py [-h] {run,alt} ...
test.py: error: invalid choice: 'blah' (choose from 'run', 'alt')
In python 2.7, you can override the error behaviour in a subclass (a shame there isn't a nicer way to differentiate the error):
import argparse
class ExceptionArgParser(argparse.ArgumentParser):
def error(self, message):
if "invalid choice" in message:
# throw exception (of your choice) to catch
raise RuntimeError(message)
else:
# restore normal behaviour
super(ExceptionArgParser, self).error(message)
parser = ExceptionArgParser()
subparsers = parser.add_subparsers(title='Modes', dest='mode')
default_parser = subparsers.add_parser('default')
default_parser.add_argument('a', nargs="+")
other_parser = subparsers.add_parser('other')
other_parser.add_argument('b', nargs="+")
try:
args = parser.parse_args()
except RuntimeError:
args = default_parser.parse_args()
# force the mode into namespace
setattr(args, 'mode', 'default')
print args
Here's another solution using a helper function to build a list of known subcommands:
import argparse
def parse_args(argv):
parser = argparse.ArgumentParser()
commands = []
subparsers = parser.add_subparsers(dest='command')
def add_command(name, *args, **kwargs):
commands.append(name)
return subparsers.add_parser(name, *args, **kwargs)
hi = add_command("hi")
hi.add_argument('--name')
add_command("hola")
# check for default command
if not argv or argv[0] not in commands:
argv.insert(0, "hi")
return parser.parse_args(argv)
assert parse_args([]).command == 'hi'
assert parse_args(['hi']).command == 'hi'
assert parse_args(['hi', '--name', 'John']).command == 'hi'
assert parse_args(['hi', '--name', 'John']).name == 'John'
assert parse_args(['--name', 'John']).command == 'hi'
assert parse_args(['hola']).command == 'hola'
You can add an argument with a default value that will be used when nothing is set I believe.
See this: http://docs.python.org/dev/library/argparse.html#default
Edit:
Sorry, I read your question a bit fast.
I do not think you would have a direct way of doing what you want via argparse. But you could check the length of sys.argv and if its length is 1 (only script name) then you could manually pass the default parameters for parsing, doing something like this:
import argparse
a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')
if len(sys.argv) == 1:
a.parse_args(['hi'])
else:
a.parse_args()
I think that should do what you want, but I agree it would be nice to have this out of the box.
For later reference:
...
b = a.add_subparsers(dest='cmd')
b.set_defaults(cmd='hey') # <-- this makes hey as default
b.add_parser('hi')
so, these two will be same:
python main.py hey
python main.py