AlyT
AlyT

Reputation: 291

How to make Python argparse accept a "-a" flag in place of the positional argument?

I am writing a Python program using argparse. I have an argument for an ID value. The user can specify an ID value to be processed in the program. Or they can specify -a to specify that all IDs should be processed.

So, both of the following should be valid:

myprog 5
myprog -a

But if you haven't specified a specific ID, then -a is required and it should throw an error.

I have played around with a mutually exclusive group:

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-a', action='store_true', help=argparse.SUPPRESS)
group.add_argument("ID", action='store', nargs='?')

Which works but my parsed args end up being two arguments:

{'a': True, 'ID': None}

and if I try to add a similar group after that, say for another argument "max" that can be a max value or -i to mean ignore a max value:

group2 = parser.add_mutually_exclusive_group(required=True)
group2.add_argument('-i', action='store_true', help=argparse.SUPPRESS)
group2.add_argument("max", action='store', nargs='?')

Then if I try to parse arguments ['-a', '2'] it throws an error saying:

usage: args_exclusive_group.py [-h] [ID] [max]
args_exclusive_group.py: error: argument ID: not allowed with argument -a

Because it is treating the 2 as ID instead of as max. Is there something really easy that I am missing that would just allow a specified positional argument (ID or max) to also take a string that happens to "look like" an optional because it starts with "-"?

Upvotes: 0

Views: 1424

Answers (2)

Tomerikoo
Tomerikoo

Reputation: 19404

If you want to keep it as 2 positional arguments, one approach might be to encapsulate the -a and -i flags inside their respective arguments and do some post-processing. Problem with that is that argparse will automatically consider strings starting with - as arguments:

positional arguments may only begin with - if they look like negative numbers and there are no options in the parser that look like negative numbers.

So if you change your keywords to say, all and ign, you can do something like:

parser = argparse.ArgumentParser()
parser.add_argument("ID")
parser.add_argument("max")

args = parser.parse_args()

if args.ID == 'all':
    print("processing all")
elif args.ID.isdigit():
    print(f"processing {args.ID}")
else:
    parser.error("ID must be a number or 'all' to use all IDs")

if args.max == 'ign':
    print("ignoring max")
elif args.max.isdigit():
    print(f"max is {args.max}")
else:
    parser.error("max must be a number or 'ign' to disable max")

And some run examples will be:

>>> tests.py 5 ign
processing 5
ignoring max

>>> tests.py all 7
processing all
max is 7

>>> tests.py blah 7
tests.py: error: ID must be a number or 'all' to use all IDs

>>> tests.py 5 blah
tests.py: error: max must be a number or 'ign' to disable max

If you really really must use -a and -i:

you can insert the pseudo-argument '--' which tells parse_args() that everything after that is a positional argument

Just change the parsing line to:

import sys
...
args = parser.parse_args(['--'] + sys.argv[1:])

Upvotes: 1

chepner
chepner

Reputation: 530970

The simplest thing would be to have just a single positional argument, whose value is either a special token like all or the number of a particular process. You can handle this with a custom type.

def process_id(s):
    if s == "all":
        return s

    try:
        return int(s)
    except ValueError:
        raise argparse.ArgumentTypeError("Must be 'all' or an integer")

p = argparse.ArgumentParser()
p.add_argument("ID", type=process_id)

args = p.parse_args()
if args.ID == "all":
    # process everything
else:
    # process just args.ID

Upvotes: 0

Related Questions