Declaring a positional argument inside a group of optional arguments in Python - python

I want to use argparse in Python to declare arguments as the following:
./get_efms_by_ids [-h] [-v] [inputfile [1 3 4 9] [-c 11..18] [20 25 40]]
What I want to do in this case are:
If inputfile is used, one can take two type of optional arguments: 1 3 4 9 or c 11..18 or both of them. If I do not enter inputfile, the optional arguments must be absent.
For example:
I can show you some examples of command line usage:
./get_efms_by_ids Vacf.txt // default: get 1 or 10 first lines in Vacf.txt
./get_efms_by_ids Vacf.txt 1 3 4 9 // get the lines that indexes: 1 3 4 9 in Vacf.txt
./get_efms_by_ids Vacf.txt c 11..18 22 25 29 // get the lines that indexes are from 11 to 18, then the lines 22, 25, 29
./get_efms_by_ids c 11.. 18 // shows a readable error message
./get_efms_by_ids 1 3 4 9 // shows a readable error message
One can use args='?' or args='*' like in the following example:
parser = argparse.ArgumentParser(description='Selecting some Elementary Flux Modes by indexes.',version='1.0')
parser.add_argument('efm_matrix_file', type=file, help='give the name of the efms matrix file')
parser.add_argument('ids', nargs='?', help='give the indexes of the chosen efms')
parser.add_argument('-i','--indexes',nargs='*', help='give the begin and start indexes of the chosen efms')
But the result did not satisfy with the purpose have proposed in the beginning of this post.
Any help will be appreciated.

First, I would ditch the -c option. You don't need both -c and .. to indicate a range of values. This would simplify your call to something like
./get_efms_by_ids [-h] [-v] [inputfile [index ...]]
where each index can be either a single integer or a range specified by lower..upper.
The argument parser could then be a simple as
def index_type(s):
try:
return int(s)
except ValueError:
try:
return map(int, s.split(".."))
except:
raise ArgumentTypeError("Invalid index: %s" % (s,))
p = ArgumentParser()
p.add_argument("-h")
p.add_argument("-v")
p.add_argument("inputfile", nargs="?")
p.add_argument("indices", nargs="*", type=index_type)
args = p.parse_args()
if not (args.inputfile is None or os.path.exists(args.inputfile)):
sys.exit("Invalid file name: %s" % (args.inputfile,))
You'll have to check that the first positional argument (if any) is a valid file or not after parsing, since any arbitrary string could be a valid file name.
The index_type function is just an example of how you could transform each index (whether an integer or range) during the course of parsing.

I take a different approach from chepner, but borrow some of chepner's ideas: ditching the -c option and use a modified index_type().
Code
#!/usr/bin/env python
import argparse
from itertools import chain
def index_type(s):
try:
return [int(s)]
except ValueError:
try:
start, stop = map(int, s.split('..'))
return range(start, stop + 1)
except:
raise argparse.ArgumentTypeError("Invalid index: %s" % (s,))
def get_options():
parser = argparse.ArgumentParser()
parser.add_argument('-v')
parser.set_defaults(fileinput=None)
options, remaining = parser.parse_known_args()
if remaining:
parser = argparse.ArgumentParser()
parser.add_argument('fileinput', type=argparse.FileType())
parser.add_argument('selected_lines', nargs='*', type=index_type)
parser.parse_args(remaining, namespace=options)
# Convert a nested list into a set of line numbers
options.selected_lines = set(chain.from_iterable(options.selected_lines))
# If the command line does not specify the line numbers, assume a default
if not options.selected_lines:
options.selected_lines = set(index_type('1..10'))
return options
if __name__ == '__main__':
options = get_options()
# If the command line contains a file name, loop through the file and process only the lines
# requested
if options.fileinput is not None:
for line_number, line in enumerate(options.fileinput, 1):
if line_number in options.selected_lines:
line = line.rstrip()
print '{:>4} {}'.format(line_number, line)
Discussion
The argparse module allows for optional argument, but fileinput cannot be optional because it is a positional argument--that is how argparse operates
To get around this limitation, I parse the command line twice: the first time to get the -v flag. For the first parsing, I use the parse_known_args() method, which ignores those parameters it does not understand.
For the second parsing, I work on the remaning arguments, assuming the first argument is the file name, followed by a series of lines numbers
Parsing line numbers is tricky. The ultimate goal is to convert something like "11..18 1 3 4 9" into [1, 3, 4, 9, 11, 12, 13, 14, 15, 16, 17, 18]
Using a modified index_type() (thanks to chepner), I was able to parse the command line from "11..18 1 3 4 9" to [11, 12, 13, 14, 15, 16, 17, 18], [1], [3], [4], [9]]
The next step is to convert this nested list into a set of line numbers for easy look up
As a bonus, if the command line does not specify any line number, I assume 1..10
After get_options returns, options.fileinput will either be None or a file handle--no need to open the file to read. options.selected_lines will be a set of line numbers to select
The final task is to go through the lines, if it is selected, process it. In my case, I just print it out

Related

Variable-length argparse argument lists

I want to have a set of arguments be passed into a script with an equal amount of inputs and outputs arguments. I know that I can parse along the lines of
inputs, outputs = sys.argv[:halfway], sys.argv[halfway:]
taking into account sys.argv[0] being the name, but I want the helpful features of argparse.
I also know that I can change the code to parser.add_argument('-i', 'inputs', nargs='+') so that I can specify my arguments as python testarg.py -i 1 2 -o 3 4, but I do not want to use that syntax as there is already a precedent of one-input, one-output python testarg.py input output which I would like to keep by making the syntax python testarg.py inputs[...] outputs[...]
This is the closest I get
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('inputs', nargs='+')
parser.add_argument('outputs', nargs='+')
print(parser.parse_args())
$ python testarg.py 1
usage: testarg.py [-h] input [input ...] output [output ...]
testarg.py: error: the following arguments are required: output
$ python testarg.py 1 2
Namespace(inputs=['1'], outputs=['2'])
$ python testarg.py 1 2 3 4
Namespace(inputs=['1', '2', '3'], outputs=['4'])
I want
Namespace(inputs=['1', '2'], outputs=['3', '4'])
The click library can do this, it supports callback functions that can modify argument values.
import click
def split_args(context, param, value):
num_args = len(value)
if num_args % 2:
raise click.BadParameter(
f"Must provide an even number of arguments, got {num_args} arguments"
)
midpoint = num_args // 2
return value[:midpoint], value[midpoint:]
#click.command()
#click.argument("args", callback=split_args, nargs=-1)
def io(args):
inputs, outputs = args
print("inputs: ", inputs)
print("outputs: ", outputs)
if __name__ == "__main__":
io()
$ python3 testarg.py 1 2 3 4
inputs: ('1', '2')
outputs: ('3', '4')
The nargs are modelled on (and even use) the regex wildcard quantifiers
In this case:
Namespace(inputs=['1', '2', '3'], outputs=['4'])
one value has been allocated to outputs (because it is "one-or-more"), and the rest, the "more" goes to inputs.
Now if you could accept
python prog.py --inputs 1 2 --outputs 3 4
the '+' would work as expected.
But with variable length positionals (or optional followed by positional), there's no way to tell it where the first list ends and second starts.
Of course if you like the argparse help, you could adjust the lists balance after parsing - e.g move the '3' to the other list. Nothing wrong with tweaking the parsed values. You won't get extra "good boy" points for doing everything in the parser itself.

Variables in Python functions on RPi

sudo python yantest.py 255,255,0
who = sys.argv[1]
print sys.argv[1]
print who
print 'Number of arguments:', len(sys.argv), 'arguments.'
print 'Argument List:', str(sys.argv)
yanon(strip, Color(who))
output from above is
255,255,0
255,255,0
Number of arguments: 2 arguments.
Argument List: ['yantest.py', '255,255,0']
Traceback (most recent call last):
File "yantest.py", line 46, in <module>
yanon(strip, Color(who))
TypeError: Color() takes at least 3 arguments (1 given)
Segmentation fault
How do I use the variable "who" inside the Color function?
Ive tried ('who'), ("who") neither of which work either.
TypeError: Color() takes at least 3 arguments (1 given)
Error means that you should pass 3 arguments but you only pass 1 argument. Here are two ways to implement:
color_r = sys.argv[1]
color_g = sys.argv[2]
color_b = sys.argv[3]
yanon(strip, Color(color_r, color_g, color_b))
Run script as:
sudo python yantest.py 255 255 0
OR
who = sys.argv[1].split(',')
yanon(strip, Color(who[0], who[1], who[2]))
Run script as:
sudo python yantest.py 255,255,0
And you should care about the type of argument!
who is a string. I don't know what type of variable color should get but probably int. You should split who string to 3 sub strings by "," and convert each one to int or whatever it should be.

Variable number of arguments to Python script

The command line to run my Python script is:
./parse_ms.py inputfile 3 2 2 2
the arguments are an input, number 3 is the number of samples of my study each with 2 individuals.
In the script, I indicate the arguments as follows:
inputfile = open(sys.argv[1], "r")
nsam = int(sys.argv[2])
nind1 = int(sys.argv[3])
nind2 = int(sys.argv[4])
nind3 = int(sys.argv[5])
However, the number of samples may vary. I can have:
./parse_ms.py input 4 6 8 2 20
in this case, I have 4 samples with 6, 8, 2 and 20 individuals in each.
It seems inefficient to add another sys.argv everything a sample is added. Is there a way to make this more general? That is, if I write nsam to be equal to 5, automatically, Python excepts five numbers to follow for the individuals in each sample.
You can simply slice off the rest of sys.argv into a list. e.g.
inputfile = open(sys.argv[1], "r")
num_samples = int(sys.argv[2])
samples = sys.argv[3:3+num_samples]
Although if that is all your arguments, you can simply not pass a number of samples and just grab everything.
inputfile = open(sys.argv[1], "r")
samples = sys.argv[2:]
Samples can be converted to the proper datatype afterward.
Also, look at argparse for a nicer way of handling command line arguments in general.
You can have a list of ninds and even catch expections doing the following
try:
ninds = [int(argv[i+3]) for i in range(int(argv[2]))]
except IndexError:
print("Error. Expected %s samples and got %d" %(argv[2], len(argv[3:])))

Accept a range of numbers in the form of 0-5 using Python's argparse?

Using argparse, is there a way to accept a range of numbers and convert them into a list?
For example:
python example.py --range 0-5
Is there some way input a command line argument in that form and end up with:
args.range = [0,1,2,3,4,5]
And also have the possibility to input --range 2 = [2]?
You could just write your own parser in the type argument, e.g.
from argparse import ArgumentParser, ArgumentTypeError
import re
def parseNumList(string):
m = re.match(r'(\d+)(?:-(\d+))?$', string)
# ^ (or use .split('-'). anyway you like.)
if not m:
raise ArgumentTypeError("'" + string + "' is not a range of number. Expected forms like '0-5' or '2'.")
start = m.group(1)
end = m.group(2) or start
return list(range(int(start,10), int(end,10)+1))
parser = ArgumentParser()
parser.add_argument('--range', type=parseNumList)
args = parser.parse_args()
print(args)
~$ python3 z.py --range m
usage: z.py [-h] [--range RANGE]
z.py: error: argument --range: 'm' is not a range of number. Expected forms like '0-5' or '2'.
~$ python3 z.py --range 2m
usage: z.py [-h] [--range RANGE]
z.py: error: argument --range: '2m' is not a range of number. Expected forms like '0-5' or '2'.
~$ python3 z.py --range 25
Namespace(range=[25])
~$ python3 z.py --range 2-5
Namespace(range=[2, 3, 4, 5])
You can just use a string argument and then parse it with range(*rangeStr.split(',')).

Unable to have a command line parameter in Python

I run
import sys
print "x \tx^3\tx^3+x^3\t(x+1)^3\tcube+cube=cube+1"
for i in range(sys.argv[2]): // mistake here
cube=i*i*i
cube2=cube+cube
cube3=(i+1)*(i+1)*(i+1)
truth=(cube2==cube3)
print i, "\t", cube, "\t", cube + cube, "\t", cube3, "\t", truth
I get
Traceback (most recent call last):
File "cube.py", line 5, in <module>
for i in range(sys.argv[2]):
IndexError: list index out of range
How can you use command line parameter as follows in the code?
Example of the use
python cube.py 100
It should give
x x^3 x^3+x^3 (x+1)^3 cube+cube=cube+1
0 0 0 1 False
1 1 2 8 False
2 8 16 27 False
--- cut ---
97 912673 1825346 941192 False
98 941192 1882384 970299 False
99 970299 1940598 1000000 False
Use:
sys.argv[1]
also note that arguments are always strings, and range expects an integer.
So the correct code would be:
for i in range(int(sys.argv[1])):
You want int(sys.argv[1]) not 2.
Ideally you would check the length of sys.argv first and print a useful error message if the user doesn't provide the proper arguments.
Edit: See http://www.faqs.org/docs/diveintopython/kgp_commandline.html
Here are some tips on how you can often solve this type of problem yourself:
Read what the error message is telling you: "list index out of range".
What list? Two choices (1) the list returned by range (2) sys.argv
In this case, it can't be (1); it's impossible to get that error out of
for i in range(some_integer) ... but you may not know that, so in general, if there are multiple choices within a line for the source of an error, and you can't see which is the cause, split the line into two or more statements:
num_things = sys.argv[2]
for i in range(num_things):
and run the code again.
By now we know that sys.argv is the list. What index? Must be 2. How come that's out of range? Knowledge-based answer: Because Python counts list indexes from 0. Experiment-based answer: Insert this line before the failing line:
print list(enumerate(sys.argv))
So you need to change the [2] to [1]. Then you will get another error, because in range(n) the n must be an integer, not a string ... and you can work through this new problem in a similar fashion -- extra tip: look up range() in the docs.
I'd like to suggest having a look at Python's argparse module, which is a giant improvement in parsing commandline parameters - it can also do the conversion to int for you including type-checking and error-reporting / generation of help messages.
Its sys.argv[1] instead of 2. You also want to makes sure that you convert that to an integer if you're doing math with it.
so instead of
for i in range(sys.argv[2]):
you want
for i in range(int(sys.argv[1])):

Categories