Currently I have a CLI tool that I'm building, and I want to give it this form similar to this.
usage: PROG SUBPARSER [-h]
(-l {optionA,optionB,optionC} | -s TERM [-a, [-b, [-c]]])
What I'm doing is I have a main cli module, which will import all the argument_parser function from all the modules I want to expose to the user, and it dynamically adds them to the main parser as sub parsers
the python code bellow it's a bit from a function that adds the parser to the main parser, being parser an object from type ArgumentParser which could be a root parser, or a sub parser. (I do this to each module, so they have their methods exposed as CLI).
Now what I'm trying to do in this particular case, is to have a command let's say PROG with a first argument SUBPARSER that has two (possibly more) mutually exclusive sequences of arguments (without creating a new sub parsers), saying I have two functions, search and list so
search and list could have common arguments (which will be assing to to the sub parser not the group) but there is also flags and arguments that are for use exclusively with --list or --search, in order to build commands like
PROG SUBARSER --list optionA -a -o -b
PROG SUBARSER --list optionA -a -o
PROG SUBARSER --list optionA -a -b
PROG SUBARSER --list optionA -a
PROG SUBARSER --list optionA
PROG SUBARSER --search TERM -a -k
PROG SUBARSER --search TERM -c
PROG SUBARSER --search TERM
I tried adding nested groups, with mutually exclusive and regular groups to the parser, but It doesn't allow me to (or at least I haven't found the way), to have mutually exclusive groups with multiple arguments, not just one flag or attribute.
This is what I have so far, that doesn't crash and actually runs usefully.
usage: PROG SUBPARSER [-h]
[-l {all,draft,staged,publish,build,not-build} | -s SEARCH]
def argument_parser(parser):
"""Argument parser for SUBPARSER subgroup"""
group = parser.add_mutually_exclusive_group(required=False)
group.add_argument('-l','--list',
choices=status_criterias,
help='List the content specified',
default='all'
)
group.add_argument('-s','--search',
help='Search by title from all the content')
Please don't mind the help strings.
Any help?
Related
What I need is:
pro [-a xxx | [-b yyy -c zzz]]
I tried this but does not work. Could someone help me out?
group= parser.add_argument_group('Model 2')
group_ex = group.add_mutually_exclusive_group()
group_ex.add_argument("-a", type=str, action = "store", default = "", help="test")
group_ex_2 = group_ex.add_argument_group("option 2")
group_ex_2.add_argument("-b", type=str, action = "store", default = "", help="test")
group_ex_2.add_argument("-c", type=str, action = "store", default = "", help="test")
Thanks!
add_mutually_exclusive_group doesn't make an entire group mutually exclusive. It makes options within the group mutually exclusive.
What you're looking for is subcommands. Instead of prog [ -a xxxx | [-b yyy -c zzz]], you'd have:
prog
command 1
-a: ...
command 2
-b: ...
-c: ...
To invoke with the first set of arguments:
prog command_1 -a xxxx
To invoke with the second set of arguments:
prog command_2 -b yyyy -c zzzz
You can also set the sub command arguments as positional.
prog command_1 xxxx
Kind of like git or svn:
git commit -am
git merge develop
Working Example
# create the top-level parser
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='help for foo arg.')
subparsers = parser.add_subparsers(help='help for subcommand', dest="subcommand")
# create the parser for the "command_1" command
parser_a = subparsers.add_parser('command_1', help='command_1 help')
parser_a.add_argument('a', type=str, help='help for bar, positional')
# create the parser for the "command_2" command
parser_b = subparsers.add_parser('command_2', help='help for command_2')
parser_b.add_argument('-b', type=str, help='help for b')
parser_b.add_argument('-c', type=str, action='store', default='', help='test')
Test it
>>> parser.print_help()
usage: PROG [-h] [--foo] {command_1,command_2} ...
positional arguments:
{command_1,command_2}
help for subcommand
command_1 command_1 help
command_2 help for command_2
optional arguments:
-h, --help show this help message and exit
--foo help for foo arg.
>>>
>>> parser.parse_args(['command_1', 'working'])
Namespace(subcommand='command_1', a='working', foo=False)
>>> parser.parse_args(['command_1', 'wellness', '-b x'])
usage: PROG [-h] [--foo] {command_1,command_2} ...
PROG: error: unrecognized arguments: -b x
Good luck.
While Jonathan's answer is perfectly fine for complex options, there is a very simple solution which will work for the simple cases, e.g. 1 option excludes 2 other options like in
command [- a xxx | [ -b yyy | -c zzz ]]
or even as in the original question:
pro [-a xxx | [-b yyy -c zzz]]
Here is how I would do it:
parser = argparse.ArgumentParser()
# group 1
parser.add_argument("-q", "--query", help="query")
parser.add_argument("-f", "--fields", help="field names")
# group 2
parser.add_argument("-a", "--aggregation", help="aggregation")
I am using here options given to a command line wrapper for querying a mongodb. The collection instance can either call the method aggregate or the method find with to optional arguments query and fields, hence you see why the first two arguments are compatible and the last one isn't.
So now I run parser.parse_args() and check it's content:
args = parser.parse_args()
if args.aggregation and (args.query or args.fields):
print "-a and -q|-f are mutually exclusive ..."
sys.exit(2)
Of course, this little hack is only working for simple cases and it would become a nightmare to check all the possible options if you have many mutually exclusive options and groups. In that case you should break your options in to command groups like Jonathan suggested.
There is a python patch (in development) that would allow you to do this.
http://bugs.python.org/issue10984
The idea is to allow overlapping mutually exclusive groups. So usage might look like:
pro [-a xxx | -b yyy] [-a xxx | -c zzz]
Changing the argparse code so you can create two groups like this was the easy part. Changing the usage formatting code required writing a custom HelpFormatter.
In argparse, action groups don't affect the parsing. They are just a help formatting tool. In the help, mutually exclusive groups only affect the usage line. When parsing, the parser uses the mutually exclusive groups to construct a dictionary of potential conflicts (a can't occur with b or c, b can't occur with a, etc), and then raises an error if a conflict arises.
Without that argparse patch, I think your best choice is to test the namespace produced by parse_args yourself (e.g. if both a and b have nondefault values), and raise your own error. You could even use the parser's own error mechanism.
parser.error('custom error message')
If you don't want subparsers, this can currently be done with mutually exclusive groups, but fair warning, it involves accessing private variables so use it at your own risk. The idea is you want -a to be mutually exclusive with -b and -c, but -b and -c don't want to be mutually exclusive with each other
import argparse
p = argparse.ArgumentParser()
# first set up a mutually exclusive group for a and b
g1 = p.add_mutually_exclusive_group()
arg_a = g1.add_argument('-a') # save this _StoreAction for later
g1.add_argument('-b')
# now set up a second group for a and c
g2 = p.add_mutually_exclusive_group()
g2.add_argument('-c')
g2._group_actions.append(arg_a) # this is the magic/hack
Now we've got -a exclusive to both -c and -b.
a = p.parse_args(['-a', '1'])
# a.a = 1, a.b = None, a.c = None
a = p.parse_args(['-a', '1', '-b', '2'])
# usage: prog.py [-h] [-a A | -b B] [-c C]
# prog.py: error: argument -b: not allowed with argument -a
Note, it does mess up the help message, but you could probably override that, or just ignore it because you've got the functionality you want, which is probably more important anyway.
If you want to ensure if we're using any of b and c, we have to use both of them, then simply add the required=True keyword arg when instantiating the mutually exclusive groups.
I'm using Python's argparse module to parse command line arguments. Consider the following simplified example,
# File test.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-s', action='store')
parser.add_argument('-a', action='append')
args = parser.parse_args()
print(args)
which can successfully be called like
python test.py -s foo -a bar -a baz
A single argument is required after -s and after each -a, which may contain spaces if we use quotation. If however an argument starts with a dash (-) and does not contain any spaces, the code crashes:
python test.py -s -begins-with-dash -a bar -a baz
error: argument -s: expected one argument
I get that it interprets -begins-with-dash as the beginning of a new option, which is illegal as -s has not yet received its required argument. It's also pretty clear though that no option with the name -begins-with-dash has been defined, and so it should not interpret it as an option in the first place. How can I make argparse accept arguments with one or more leading dashes?
You can force argparse to interpret an argument as a value by including an equals sign:
python test.py -s=-begins-with-dash -a bar -a baz
Namespace(a=['bar', 'baz'], s='-begins-with-dash')
If you are instead trying to provide multiple values to one argument:
parser.add_argument('-a', action='append', nargs=argparse.REMAINDER)
will grab everything after -a on the command line and shove it in a.
python test.py -toe -a bar fi -fo -fum -s fee -foo
usage: test.py [-h] [-s S] [-a ...]
test.py: error: unrecognized arguments: -toe
python test.py -a bar fi -fo -fum -s fee -foo
Namespace(a=[['bar', 'fi', '-fo', '-fum', '-s', 'fee', '-foo']], s=None)
Note that even though -s is a recognized argument, argparse.REMAINDER adds it to the list of args found by -a since it is after -a on the command line
I use argparser as a generic way to provide inputs on multiple different files, that are used to generate at the end a json, and sent to a database.
Have that said, I want to use multiple mutually exclusive groups, with the option to a flag being part of multiple different groups (as in the example below).
parser = argparser.argumentParser
group1 = parser.add_mutually_exclusive_group()
group2 = parser.add_mutually_exclusive_group(required=True)
group3 = parser.add_mutually_exclusive_group()
group1.add_argument('-a', type=int)
group1.add_argument('-d', type=int)
group2.add_argument('-z', type=int)
group2.add_argument('-x', type=int)
group3.add_argument('-a', type=int)
group3.add_argument('-z', type=int)
it means that -d and -z can go together (but -z or -x are mandatory), giving me the option to have -a -d -x OR -z -d
for some reason, the argparser thinks each one of the -a or -z flags are conflicting, so i have added the conflict_handler to 'resolve', but seems to have no effect
When you add an argument to a group, it is also added to the parser. Groups, both argument_group and mutually_exclusive_group are ways of defining some special actions (in help and testing), but they don't change the fundamental parsing.
So the arguments you try to define via group3 conflict with the arguments already defined via the other groups. I should also note that add_argument creates an argument Action object.
For a bug/issue I came up with a way of adding pre existing Actions to a new group. That is, a way of adding the -a and -z that were created earlier to group3. Actually I wrote it as a way defining a group with a list of existing Actions. That wasn't very hard to do. But displaying such a group required a major rewrite to the usage formatter.
https://bugs.python.org/issue10984
mutually_exclusive_group does 2 things - it modifies the usage - if possible. And it does the 'mutually-exclusive' test. Otherwise it does not modify the parsing. You could perform the same tests after parsing.
In your example, all arguments have a default of None. So after parsing, you could do:
if args.a is not None and args.z is not None:
parse.error('cannot use both -a and -z')
In the bug/issue I modified add_mutually_exclusive_group to effectively do:
group1 = parser.add_mutually_exclusive_group()
group2 = parser.add_mutually_exclusive_group(required=True)
a1 = group1.add_argument('-a', type=int) # hang onto the newly created Action
group1.add_argument('-d', type=int)
a2 = group2.add_argument('-z', type=int)
group2.add_argument('-x', type=int)
group3 = parser.add_mutually_exclusive_group()
group3._group_actions.append(a1) # add existing Action to group
group3._group_actions.append(a2)
#group3.add_argument('-a', type=int)
#group3.add_argument('-z', type=int)
That is, pointers to the existing Actions are added directly to the new group, without going through add_argument.
testing group3:
2347:~/mypy$ python3 stack47670008.py -z 3 -a3
usage: stack47670008.py [-h] [-a A | -d D] (-z Z | -x X)
stack47670008.py: error: argument -a: not allowed with argument -z
2347:~/mypy$ python3 stack47670008.py -z 3 -d3
Namespace(a=None, d=3, x=None, z=3)
2347:~/mypy$ python3 stack47670008.py -h
usage: stack47670008.py [-h] [-a A | -d D] (-z Z | -x X)
optional arguments:
-h, --help show this help message and exit
-a A
-d D
-z Z
-x X
group1 and group2 show up in the usage, but not group3.
I think what you're looking for is the sub_parsers option from argparse.
Kindly check the link from the python docs add_subparsers.
I am writing a program which, among other things, allows the user to specify through an argument a module to load (and then use to perform actions). I am trying to set up a way to easily pass arguments through to this inner module, and I was attempting to use ArgParse's action='append' to have it build a list of arguments that I would then pass through.
Here is a basic layout of the arguments that I am using
parser.add_argument('-M', '--module',
help="Module to run on changed files - should be in format MODULE:CLASS\n\
Specified class must have function with the signature run(src, dest)\
and return 0 upon success",
required=True)
parser.add_argument('-A', '--module_args',
help="Arg to be passed through to the specified module",
action='append',
default=[])
However - if I then try to run this program with python my_program -M module:class -A "-f filename" (where I would like to pass through the -f filename to my module) it seems to be parsing the -f as its own argument (and I get the error my_program: error: argument -A/--module_args: expected one argument
Any ideas?
With:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-M', '--module',
help="Module to run on changed files - should be in format MODULE:CLASS\n\
Specified class must have function with the signature run(src, dest)\
and return 0 upon success",
)
parser.add_argument('-A', '--module_args',
help="Arg to be passed through to the specified module",
action='append',
default=[])
import sys
print(sys.argv)
print(parser.parse_args())
I get:
1028:~/mypy$ python stack45146728.py -M module:class -A "-f filename"
['stack45146728.py', '-M', 'module:class', '-A', '-f filename']
Namespace(module='module:class', module_args=['-f filename'])
This is using a linux shell. The quoted string remains one string, as seen in the sys.argv, and is properly interpreted as an argument to -A.
Without the quotes the -f is separate and interpreted as a flag.
1028:~/mypy$ python stack45146728.py -M module:class -A -f filename
['stack45146728.py', '-M', 'module:class', '-A', '-f', 'filename']
usage: stack45146728.py [-h] [-M MODULE] [-A MODULE_ARGS]
stack45146728.py: error: argument -A/--module_args: expected one argument
Are you using windows or some other OS/shell that doesn't handle quotes the same way?
In Argparse `append` not working as expected
you asked about a slightly different command line:
1032:~/mypy$ python stack45146728.py -A "-k filepath" -A "-t"
['stack45146728.py', '-A', '-k filepath', '-A', '-t']
usage: stack45146728.py [-h] [-M MODULE] [-A MODULE_ARGS]
stack45146728.py: error: argument -A/--module_args: expected one argument
As I already noted -k filepath is passed through as one string. Because of the space, argparse does not interpret that as a flag. But it does interpret the bare '-t' as a flag.
There was a bug/issue about the possibility of interpreting undefined '-xxx' strings as arguments instead of flags. I'd have to look that up to see whether anything made it into to production.
Details of how strings are categorized as flag or argument can be found in argparse.ArgumentParser._parse_optional method. It contains a comment:
# if it contains a space, it was meant to be a positional
if ' ' in arg_string:
return None
http://bugs.python.org/issue9334 argparse does not accept options taking arguments beginning with dash (regression from optparse) is an old and long bug/issue on the topic.
The solution is to accept arbitrary arguments - there's an example in argparse's doc here:
argparse.REMAINDER. All the remaining command-line arguments are gathered into a list. This is commonly useful for command line utilities that dispatch to other command line utilities:
>>> parser = argparse.ArgumentParser(prog='PROG')
>>> parser.add_argument('--foo')
>>> parser.add_argument('command')
>>> parser.add_argument('args', nargs=argparse.REMAINDER)
>>> print(parser.parse_args('--foo B cmd --arg1 XX ZZ'.split()))
Namespace(args=['--arg1', 'XX', 'ZZ'], command='cmd', foo='B')
I would like to make the parser like cmd [-a xxx -b xxx] -c xxx -d xxx
When -a is used, I want -b to be used too. likewise, if -b is used, -a must be used too. It's ok both -a and -b are not used.
How do I do that? I have tried custom actions, but it does not go well.
A better design would be to have a single option that takes two arguments:
parser.add_argument('-a', nargs=2)
Then you either specify the option with 2 arguments, or you don't specify it at all.
$ script -a 1 2
or
$ script
A custom action (or postprocessing) can split the tuple args.a into two separate values args.a and args.b.
Argparse doesn't natively support this type of use.
The most effective thing to do is check and see if those types of conditions are met after parsing:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-b')
parser.add_argument('-c')
args = parser.parse_args()
required_together = ('b','c')
# args.b will be None if b is not provided
if not all([getattr(args,x) for x in required_together]):
raise RuntimeError("Cannot supply -c without -b")