119

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!

Sean
  • 4,267
  • 10
  • 36
  • 50
  • possible duplicate of [How to make python argparse mutually exclusive group arguments without prefix?](http://stackoverflow.com/questions/7869345/how-to-make-python-argparse-mutually-exclusive-group-arguments-without-prefix) – ennuikiller Jul 28 '13 at 14:40
  • Plugging, but I wanted to mention my library [joffrey](https://github.com/supposedly/joffrey). Lets you do what this question wants, for example, without making you use subcommands (as in the accepted answer) or validate everything yourself (as in the second-highest-voted response). –  Oct 01 '19 at 22:02

4 Answers4

129

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.

Martijn Courteaux
  • 67,591
  • 47
  • 198
  • 287
Jonathan
  • 5,736
  • 2
  • 24
  • 22
  • I have already put them under an argument group. How can I add sub-command in this case? Thanks! – Sean Jul 28 '13 at 15:02
  • 1
    Updated with sample code. You won't use groups, but subparsers. – Jonathan Jul 28 '13 at 15:14
  • 9
    But how would you do what OP originally asked? I currently have a set of sub-commands, but one of those sub-commands does need the ability to choose between `[[-a ] | [-b -c ]]` – code_dredd Jul 13 '18 at 23:14
  • 9
    This does not answer the question because it does not allow you to make "noname" commands and achieve what OP asked for `[-a xxx | [-b yyy -c zzz]]` – The Godfather May 15 '19 at 11:37
50

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.

Gringo Suave
  • 29,931
  • 6
  • 88
  • 75
oz123
  • 27,559
  • 27
  • 125
  • 187
  • 7
    I would not call this a 'hack' for this case, since it seems both more readable and manageable - thanks for pointing it out! – sage Aug 09 '16 at 18:58
  • 30
    An even better way would be to use `parser.error("-a and -q ...")`. This way complete usage help will be printed out automatically. – WGH Aug 26 '16 at 00:20
  • Please note that in this case you would also need to validate the cases like: (1) both `q` and `f` are required in first group is user, (2) either of the groups is required. And this makes "simple" solution not so simple any more. So I would agree that this is more hack for handcrafted script, but not a real solution – The Godfather May 15 '19 at 11:36
  • if you need at least one of the two options, you can make this exclusive or by changing the if statement to `if bool(args.aggregation) is bool(args.query or args.fields):` – Edward Spencer Jan 13 '22 at 16:28
  • I don't consider this a great answer I'm afraid. The OP asked how to have mutually exclusive options, not only from the standpoint of application logic enforcing it (as is done here), but also from the user interface perspective. If you say "--help" and it says options are [-q] [-f] [-a] then there is no indication of how they're mutually exclusive until you fire an error. – Fred Douglis Feb 03 '23 at 21:56
  • Not that I don't use exactly this sort of logic in simple applications that are mostly for my own benefit. It's just that if you're trying to display useful --help, the highly-upvoted answer with subcommands works perfectly, and is barely more complicated than this answer. [This didn't fit with the previous comment it seems.] – Fred Douglis Feb 03 '23 at 21:57
5

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')
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • 1
    Python issue: http://bugs.python.org/issue11588 is exploring ways to let you write custom exclusive/inclusive tests. – hpaulj Mar 07 '14 at 18:23
  • There's no active effort to add such a feature to `argparse`. Do your own post parsing testing. – hpaulj Aug 07 '21 at 15:34
2

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.

Edward Spencer
  • 448
  • 8
  • 10
  • This amazing reply solved my issue. This is based on the native `argparse` library, so less dependencies on 3rd parties. – Ehsan May 16 '23 at 13:36