Reputation: 6532
Here's a short summary of what I am trying to achieve:
So in an ideal world I'd like to run my script in the following ways, they are all valid use cases:
python myscript.py file1 file2 file3 "key" 10
python myscript.py file1 file2 "key" 10 12
python myscript.py file1 "key" 10 12
python myscript.py file1 "key" 10
Given the use case I tried:
parser = argparse.ArgumentParser()
parser.add_argument("files", nargs='+', help="input files")
parser.add_argument("gene", help="gene of interest")
parser.add_argument("pos", nargs='+', type=int, help="position(s) to analyze")
""" Validate pos """
if len(args.pos) > 2:
sys.exit("Positions argument needs to be a single integer or two integers denoting a range!")
...
# in some other function
if len(args.pos) == 1:
key = row[SIND][args.pos[0]]
elif len(args.pos) == 2:
key = row[SIND][args.pos[0] : args.pos[1]]
else:
print(args.pos, len(args.pos))
sys.exit("args.pos assertion failed!")
It works when there is only one integer but not when I send two integers to analyze a range instead. In the latter case the "key" is interpreted as a file as well thus I get FileNotFoundError: [Errno 2] No such file or directory: 'IGHV4-39'
.
Question1: Is it possible to flag or indicate positional arguments, so that I can tell argparse when the files
parameter ends and gene
starts? I don't want to make them optional arguments, since the logic of the script is incomplete if either of the three parameters is omitted.
Question2: Is it otherwise helpful to divide the pos parameter into two; pos which takes a single integer, and range which takes two integers, then make them somehow exclusive?
Any ideas?
Upvotes: 4
Views: 3433
Reputation: 231375
Here's a quick implementation of a sys.argv
parse:
In [97]: txt='file1 file2 file3 "key" 10 12'
In [98]: argv=txt.split()
In [99]: argv
Out[99]: ['file1', 'file2', 'file3', '"key"', '10', '12']
In [104]: files,rows,key=[],[],None
In [105]: for a in argv[::-1]:
...: try:
...: rows.append(int(a))
...: except ValueError:
...: if key is None: key=a
...: else: files.append(a)
...:
In [106]: files
Out[106]: ['file3', 'file2', 'file1']
In [107]: rows
Out[107]: [12, 10]
In [108]: key
Out[108]: '"key"'
You could even use parse_known_args
to handle other flagged arguments, and apply this logic to unparse extras
.
argparse
handles arguments from left to right; your positional logic fits the reverse better. Flags (optionals
) provide well defined dividers between argument strings. Without them, parsing muliple +
arguments is impossible. The first '+' is greedy (think of regex
behavior), and grabs all. And when allocating strings to arguments, it does not check types. Type conversion (e.g. to int
) occurs after the string has been allocated.
Upvotes: 1
Reputation: 8159
"Is it possible to flag ... positional arguments?" Well, if you did that, then they wouldn't be positional arguments anymore! Since after all positional arguments are defined by being in position, not by being flagged.
I think all you really need is the required
option, and not to try and be too clever using positional arguments. Is your aim to create an amazingly clever interface that magically works out what it is you want regardless of input, or an interface that works predictably and produces good error messages?
I can be sarcastic on this point because I have spent far too much time in the past doing the first instead of just doing the second.
So why not this:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--files', required=True, nargs='+')
parser.add_argument('--genes', required=True, nargs='+')
parser.add_argument(
'--pos', required=True, nargs='+', type=int, help="position(s) to analyze")
opts = parser.parse_args()
print(dir(parser))
print('files: %s, genes: %s, poses: %s' % (
opts.files, opts.genes, opts.pos))
So:
$ python test.py
usage: test.py [-h] --files FILES [FILES ...] --genes GENES [GENES ...] --pos
POS [POS ...]
test.py: error: the following arguments are required: --files, --genes, --pos
$ python test.py --files file1 file2 --genes xzy --pos 2 1
files: ['file1', 'file2'], genes: ['xzy'], poses: [2, 1]
Aren't you essentially asking "what is the best interface here"? Surely that's something you need to decide. My advice: keep it simple. Why not do:
parser.add_argument('--start', '-s', required=True, nargs=1, type=int)
parser.add_argument('--end', '-e', nargs=1, type=int)
opts = parser.parse_args()
if opts.end is None:
opts.end = opts.start + 1
Upvotes: 5