Variable-length argparse argument lists - python

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.

Related

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.

Python Arguments and Passing Floats in Arguments

I've run into a couple of issues using arguments within a python script. Can i please get some help or direction to get this code functional? Thank you in advance.
First issue: I am unable to specify multiple arguments at once.
For example I am able to pass a single argument fine:
$ ./my_arg_scenario.py -a
Argument_A
$ ./my_arg_scenario.py -c
Argument_C
$ ./my_arg_scenario.py -d
Argument_D
However, I am looking for a way to pass multiple arguments in any position. Is there a way I can accomplish this?
For example, I would like the below to occur:
./my_arg_scenario.py -a -c -d
Argument_A
Argument_C
Argument_D
# OR
./my_arg_scenario.py -c -a
Argument_C
Argument_A
Second Issue: I am trying to pass both whole numbers and floats in the -b argument. But when I pass a float/decimal I get the below error. Is there a way I can pass both a float and whole number?
This works:
$ ./my_arg_scenario.py -b 5
The number provided is: 5
But this does NOT:
$ ./my_arg_scenario.py -b 5.50
Traceback (most recent call last):
File "./my_arg_scenario.py", line 18, in <module>
if int(sys.argv[2]) not in range(0,11):
ValueError: invalid literal for int() with base 10: '5.50'
Below is my testable code:
#!/usr/local/bin/python3.5
import sys
script_options = ['-a', '-b', '-c', '-d']
manual_flag = ''
build_flag = ''
if len(sys.argv) > 1:
if sys.argv[1] in script_options:
pass
else:
print('\n\t\tParameter "' + sys.argv[1] + '" is an invalid argument.\n')
sys.exit()
if sys.argv[1] == '-a':
print('Argument_A')
sys.exit()
elif sys.argv[1] == '-b':
if int(sys.argv[2]) not in range(0,11):
print('Invalid interval. Please select a value bewteen 1-5s.')
sys.exit()
else:
print('The number provided is: ' + (sys.argv[2]))
elif sys.argv[1] == '-c':
manual_flag = 'Argument_C'
print(manual_flag)
elif sys.argv[1] == '-d':
build_flag ='Argument_D'
print(build_flag)
else:
pass
You didn't actually provide the code you're using (aside from incidentally in the traceback),(Update: Code added later) but the answer is: Stop messing around with parsing sys.argv manually and use the argparse module (or docopt or something that doesn't involve rolling your own switch parsing).
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-a', action='store_true')
parser.add_argument('-b', metavar='INTERVAL', type=int, choices=range(11))
parser.add_argument('-c', action='store_true')
parser.add_argument('-d', action='store_true')
args = parser.parse_args()
if args.a: print('Argument_A')
if args.b is not None: print('The number provided is:', args.b)
if args.c: print('Argument_C')
if args.d: print('Argument_D')
If you want to accept int or float, the easiest solution is to just make type=float and use a consistent type (but the range check must be done outside the parsing step). If you must allow both, ast.literal_eval or a homegrown argparse type conversion function are options. Since you want a range check too (which range won't handle properly for float values that aren't equal to int values), roll a type checker:
def int_or_float(minval=None, maxval=None):
def checker(val):
try:
val = int(val)
except ValueError:
val = float(val)
if minval is not None and val < minval:
raise argparse.ArgumentTypeError('%r must be >= %r' % (val, minval))
if maxval is not None and val > maxval:
raise argparse.ArgumentTypeError('%r must be <= %r' % (val, maxval))
return val
return checker
Then use it by replacing the definition for -b with:
# Might want int_or_float(0, 10) depending on range exclusivity rules
parser.add_argument('-b', metavar='INTERVAL', type=int_or_float(0, 11))

Declaring a positional argument inside a group of optional arguments in 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

take a list of numbers from the command line and print the largest number

I have to write a program that, when given numbers from the command line, manages to read and print the highest number.
import sys
numbers = sys.argv[1]
def map(f,items):
result = []
for i in range(0,len(items),1):
result = result + [f(items[i])
return result
im trying to find the easiest way to go about doing this! Thank you!
import sys
print(max(float(x) for x in sys.argv[1:]))
Alternatively, you can use print(max(sys.argv[1:], key=float)) (thanks to the nice addition by frostnational).
Demo:
>> python a.py 12 6 3.14 11
12.0
Explanation: If we add print(sys.argv) to the script, the output for the above example will be
['a.py', '12', '6', '3.14', '11']
12.0
The solution is slicing off the first element of sys.argv, converting the other strings to floats in a generator expression, and then finding the maximum.
#timgeb answer is very good but i would also check argparse if you are using the command line interface it can give you some very good option for your current and future code, by using argparse you can save a lot of programming time and get a full help and usage messages.
I'll take the example form the docs since it's relevant for your question, this code will print either max or sum.
import argparse
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')
args = parser.parse_args()
print(args.accumulate(args.integers))
For max: just call the code with a list of numbers.
For sum: or any other option you would like to add you can use an argument, in this case --sum

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(',')).

Categories