I'm trying to make a program in Python 3.5 (the Python version shipped by Debian 9) that takes two command line arguments: an input file name and an output file name.
- The input file name must either precede the output file name or
itself be preceded by
-i. - The output file name is optional. If present, and the input file
name is not preceded by
-i, it must either follow the input file name or itself be preceded by-o.
Thus I want to accept the following command lines:
programname.py infilename
programname.py -i infilename
programname.py infilename outfilename
programname.py -i infilename outfilename
programname.py infilename -o outfilename
programname.py -i infilename -o outfilename
programname.py outfilename -i infilename
programname.py -o outfilename -i infilename
programname.py -o outfilename infilename
The usage message might look like this:
programname.py [-i] infilename [[-o] outfilename]
But I can't tell from the documentation of the argparse module how to express this in arguments to add_argument(). When I give two names for a single argument, one positional and one named, add_argument() raises an exception:
ValueError: invalid option string 'infilename': must start with a character '-'
I searched Stack Overflow for similar questions and found hpaulj's answer to Python argparse - mandatory argument - either positional or optional and hpaulj's answer to argparse: let the same required argument be positional OR optional. The construction in these answers uses a group of two mutually exclusive arguments, one positional and one named. But it doesn't appear to work with multiple such arguments. Trying to parse -i infilename outfilename with a parser built this way produces a different exception:
argparse.ArgumentError: argument INFILE: not allowed with argument -i
However, argparse itself has trouble printing this exception or even showing --help:
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
[8+ layers of method calls within `argparse.py` omitted]
File "/usr/lib/python3.5/argparse.py", line 396, in _format_actions_usage
start = actions.index(group._group_actions[0])
IndexError: list index out of range
The deprecated optparse module stored positional arguments in a separate list, which code that runs after parsing could read to fill in each argument that is None. The direct counterpart to this list in argparse is parser.add_argument('args', nargs=argparse.REMAINDER). Is handling positional arguments manually after calling parse_args() the only way to accept all command line forms shown above using argparse?
#!/usr/bin/env python3
import argparse
import traceback
def mkparser1():
"""Raise an error.
ValueError: invalid option string 'infilename': must start with a character '-'
"""
parser = argparse.ArgumentParser()
parser.add_argument("infilename", "-i", metavar="INFILE")
parser.add_argument("outfilename", "-o", required=False, metavar="INFILE")
return parser
def mkparser2():
"""Do not raise an error but return an inadequate parser.
When asked -i infilename outfilename
"""
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("infilename", nargs="?", metavar="INFILE")
group.add_argument('-i', dest="infilename", metavar="INFILE")
group = parser.add_mutually_exclusive_group(required=False)
parser.add_argument("outfilename", nargs="?", metavar="OUTFILE")
parser.add_argument("-o", dest="outfilename", metavar="OUTFILE")
return parser
def test():
parser = mkparser2()
argstrings = [
"infilename",
"-i infilename",
"infilename outfilename",
"-i infilename outfilename",
"infilename -o outfilename",
"-i infilename -o outfilename",
"outfilename -i infilename",
"-o outfilename -i infilename",
"-o outfilename infilename",
"--help",
]
for s in argstrings:
print("for", s)
try:
pargs = parser.parse_args(s.split())
except Exception as e:
traceback.print_exc()
else:
print("infilename is %s and outfilename is %s"
% (pargs.infilename, pargs.outfilename))
if __name__=='__main__':
test()
optparseis not define any positionals, and useparse_known_args. When you have two positionals with '?' (or '*' or '+'), life becomes complicated. It still tries to fill the positionals in the order that they come, without any knowledge about what optionals might have already set. Keep in mind theargparsetries to parse optionals in an order agnostic manner. - hpaulj