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))
Related
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.
This question already has answers here:
Why does passing variables to subprocess.Popen not work despite passing a list of arguments?
(5 answers)
Closed 1 year ago.
I was wondering how I can pass a python variable to subprocess.check_output command.
In this particular case I have lower and upper python variables to be passed to the subprocess.check_output command, but I'm certain the way I have done it below isn't correct because it's not giving me the expected result.
If I input the values for the lower and upper bound values manually it does work.
for qq in range (0, 5, 1):
lo = glob.glob(path2 + "IM" + path1 + "*_GM.nii.gz")
lo = ' '.join(lo)
lower = qq - 0.5
upper = qq + 0.5
subprocess.check_output(['fslstats {} -l lower -u upper -V | cut -d " " -f 1'.format(lo)], shell=True)
Any suggestions how I can pass the lower and upper variables?
Note:
lo= /Users/say/Documents/awIM/network5/awfc_GM.nii.gz
path2=/Users/say/Documents/aw
path1=/network5/awfc
Thanks
Posted Community Wiki because this is a question already asked and answered elsewhere in the knowledgebase.
Doing this correctly (but for the removal of the cut in favor of native-Python string manipulation) might look something like:
glob_str = path2 + "IM" + path1 + "*_GM.nii.gz"
glob_list = glob.glob(glob_str)
if len(glob_list) == 0:
raise Exception("No results found from glob expression %r" % glob_str)
for qq in range (0, 5, 1):
lower = qq - 0.5
upper = qq + 0.5
args = ['fslstats'] + glob_list + [ '-l', str(lower), '-u', str(upper), '-V' ]
### EVERYTHING BELOW HERE IS UNNECESSARILY COMPLICATED BY THE USE OF 'cut'
### CONSIDER REPLACING WITH THE ALTERNATE IMPLEMENTATION BELOW.
p1 = subprocess.Popen(args, stdout=subprocess.PIPE)
p1.stdout.close()
p2 = subprocess.Popen(['cut', '-d', ' ', '-f1'], stdin=p1.stdout)
(stdout, _) = p2.communicate()
if p1.wait() != 0:
raise Exception("fslstats run as %r returned exit status %r" % (args, p1.returncode))
print("Result is: %r" % (stdout.split("\n"),))
To remove cut, you might change everything below the line assigning args as follows:
stdout = subprocess.check_output(args)
first_column = [ line.split()[0] for line in stdout.split('\n') ]
print("Result is: %r" % first_column)
Note:
We're not using shell=True. Keeping this disabled makes for an implementation where you have more control -- a shell isn't doing things behind your back, and you don't need to know how that shell works and how it's implemented to avoid (potentially security-impacting) bugs.
To implement a pipeline without shell=True, we're following the practices documented at https://docs.python.org/2/library/subprocess.html#replacing-shell-pipeline
We're actually passing the values of the lower and upper variables, instead of passing the command lower and upper strings.
We're not joining our glob results into a string (which would break our command if any filenames resulting from that glob contained spaces), but are instead passing the list directly on the argument list for fslstats.
Because you care about the exit status of fslstats, not cut, you need to check that yourself. (Even with shell=True, you get default shell behavior, which returns only the exit status of the last pipeline component).
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.
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
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(',')).