17

I would like to use argparse to make some code to be used in the following two ways:

./tester.py all
./tester.py name someprocess

i.e. either all is specified OR name with some additional string.

I have tried to implement as follows:

import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument('all', action='store_true', \
        help = "Stops all processes")
group.add_argument('name', \
        help = "Stops the named process")

print parser.parse_args()

which gives me an error

ValueError: mutually exclusive arguments must be optional

Any idea how to do it right? I also would like to avoid sub parsers in this case.

4
  • Why would you like to avoid sup parsers? This looks exactly like a sub-parser problem! Commented Mar 20, 2013 at 17:06
  • They operate already on suparsers. I want to keep it shallow... But if there is no other solution I will try it with subparsers in two levels.
    – Alex
    Commented Mar 20, 2013 at 17:07
  • Change all to --all and name to --name.
    – hughdbrown
    Commented Mar 20, 2013 at 17:48
  • 3
    @hughdbrown: I know this works, but it is not what I asked.
    – Alex
    Commented Mar 20, 2013 at 18:09

5 Answers 5

16

The question is a year old, but since all the answers suggest a different syntax, I'll give something closer to the OP.

First, the problems with the OP code:

A positional store_true does not make sense (even if it is allowed). It requires no arguments, so it is always True. Giving an 'all' will produce error: unrecognized arguments: all.

The other argument takes one value and assigns it to the name attribute. It does not accept an additional process value.

Regarding the mutually_exclusive_group. That error message is raised even before parse_args. For such a group to make sense, all the alternatives have to be optional. That means either having a -- flag, or be a postional with nargs equal to ? or *. And doesn't make sense to have more than one such positional in the group.

The simplest alternative to using --all and --name, would be something like this:

p=argparse.ArgumentParser()
p.add_argument('mode', choices=['all','name'])
p.add_argument('process',nargs='?')

def foo(args):
    if args.mode == 'all' and args.process:
        pass # can ignore the  process value or raise a error
    if args.mode == 'name' and args.process is None:
        p.error('name mode requires a process')

args = p.parse_args()
foo(args) # now test the namespace for correct `process` argument.

Accepted namespaces would look like:

Namespace(mode='name', process='process1')
Namespace(mode='all', process=None)

choices imitates the behavior of a subparsers argument. Doing your own tests after parse_args is often simpler than making argparse do something special.

5
  • +1 simplest indeed, I forgot about choices, and I didn't read carefully that OP only needs one process name after name argument.
    – justhalf
    Commented Mar 24, 2014 at 7:12
  • This is not good enough. The default help gives no indication that process is required with name and ignored / disallowed with all. Eight years later nobody bothered to implement such functionality in argparse. I don't see why mutually exclusive groups couldn't contain positional arguments, as long as there is only a single positional argument in the entire argument parser (as in OP's case and mine). Commented Nov 24, 2021 at 11:12
  • @БоратСагдиев, I pointed out that one positional with the right nargs (and a default) can be used in such a group. The OP had the wrong kind of positional. Formatting the usage line for a fancy set of conditions is not a trivial task. Write your own, or use the description/epilog to explain what you require.
    – hpaulj
    Commented Nov 24, 2021 at 16:59
  • Indeed, I tried it with an option and a positional argument with nargs='?' and it seems to work, for the most part. When I first add the option to the group, and then the positional argument, the help correctly indicates that the two arguments are mutually exclusive. However, when the positional is added first and then the option, it just shows [-o] [positional] as if they could be used in conjunction, although actually using them in conjunction causes an error, as expected. Commented Nov 25, 2021 at 18:31
  • @БоратСагдиев, I don't remember the details why, but yes, tor correct usage display, the positional should be last of the group. The usage formatter is not very sophisticated and easily broken. But during parsing the groups testing is performed by unrelated code. When I explored implementing a more general nested groups mechanism some years ago, the usage formatting was a much more complicated task than either the input (defining groups) or testing.
    – hpaulj
    Commented Nov 25, 2021 at 18:39
2
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-a','--all', action='store_true', \
        help = "Stops all processes")
group.add_argument('-n','--name', \
        help = "Stops the named process")

print parser.parse_args()

./tester.py -h

usage: zx.py [-h] (-a | -n NAME)

optional arguments:
  -h, --help            show this help message and exit
  -a, --all             Stops all processes
  -n NAME, --name NAME  Stops the named process
0

"OR name with some additional string."

positional argument cannot take additional string

I think the best solution for you is (named test.py):

import argparse
p = argparse.ArgumentParser()
meg = p.add_mutually_exclusive_group()
meg.add_argument('-a', '--all', action='store_true', default=None)
meg.add_argument('-n', '--name', nargs='+')
print p.parse_args([])
print p.parse_args(['-a'])
print p.parse_args('--name process'.split())
print p.parse_args('--name process1 process2'.split())
print p.parse_args('--all --name process1'.split())

$ python test.py

Namespace(all=None, name=None)
Namespace(all=True, name=None)
Namespace(all=None, name=['process'])
Namespace(all=None, name=['process1', 'process2'])
usage: t2.py [-h] [-a | -n NAME [NAME ...]]
t2.py: error: argument -n/--name: not allowed with argument -a/--all
0

I would agree that this looks exactly like a sub-parser problem, and that if you don't want to make it an optional argument by using --all and --name, one suggestion from me would be just to ignore the all and name altogether, and use the following semantics:

  1. If tester.py is called without any arguments, stop all process.
  2. If tester.py is called with some arguments, stop only those processes.

Which can be done using:

import argparse, sys
parser = argparse.ArgumentParser()
parser.add_argument('processes', nargs='*')
parsed = parser.parse(sys.argv[1:])
print parsed

which will behave as follows:

$ python tester.py
Namespace(processes=[])
$ python tester.py proc1
Namespace(processes=['proc1'])

Or, if you insist on your own syntax, you can create a custom class. And actually you're not having a "mutually exclusive group" case, since I assume if all is specified, you will ignore the rest of the arguments (even when name is one of the other arguments), and when name is specified, anything else after that will be regarded as processes' name.

import argparse
import sys
class AllOrName(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        if len(values)==0:
            raise argparse.ArgumentError(self, 'too few arguments')
        if values[0]=='all':
            setattr(namespace, 'all', True)
        elif values[0]=='name':
            if len(values)==1:
                raise argparse.ArgumentError(self, 'please specify at least one process name')
            setattr(namespace, 'name', values[1:])
        else:
            raise argparse.ArgumentError(self, 'only "all" or "name" should be specified')

parser = argparse.ArgumentParser()
parser.add_argument('processes', nargs='*', action=AllOrName)
parsed = parser.parse_args(sys.argv[1:])
print parsed

with the following behaviour:

$ python argparse_test.py name
usage: argparse_test.py [-h] [processes [processes ...]]
argparse_test.py: error: argument processes: please specify at least one process name

$ python argparse_test.py name proc1
Namespace(name=['proc1'], processes=None)

$ python argparse_test.py all
Namespace(all=True, processes=None)

$ python argparse_test.py host
usage: argparse_test.py [-h] [processes [processes ...]]
argparse_test.py: error: argument processes: only "all" or "name" should be specified

$ python argparse_test.py
usage: argparse_test.py [-h] [processes [processes ...]]
argparse_test.py: error: argument processes: too few arguments
-2

This is probably what you're looking for:

group.add_argument('--all', dest=is_all, action='store_true')
group.add_argument('--name', dest=names, nargs='+')

Passing --name will then require at list one value and store them as a list.

0

Not the answer you're looking for? Browse other questions tagged or ask your own question.