简体   繁体   中英

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 "-"?

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:])

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM