argparse: optional argument between positional arguments - python

I want to emulate the behavior of most command-line utilities, where optional arguments can be put anywhere in the command line, including between positional arguments, such as in this mkdir example:
mkdir before --mode 077 after
In this case, we know that --mode takes exactly 1 argument, so before and after are both considered positional arguments. The optional part, --mode 077, can really be put anywhere in the command line.
However, with argparse, the following code does not work with this example:
# mkdir.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--mode', nargs=1)
parser.add_argument('dirs', nargs='*')
args = parser.parse_args()
Running ./mkdir.py before --mode 077 after results in:
mkdir.py: error: unrecognized arguments: after
How can I get argparse to accept an optional argument (with a fixed, known number of items) between positional ones?

Starting from Python 3.7, it seems argparse now supports this kind of Unix-style parsing:
Intermixed parsing
ArgumentParser.parse_intermixed_args(args=None, namespace=None)
A number of Unix commands allow the user to intermix optional arguments with positional arguments. The parse_intermixed_args() and parse_known_intermixed_args() methods support this parsing style.
There is a caveat, but for "simple" options, it does not affect them:
These parsers do not support all the argparse features, and will raise exceptions if unsupported features are used. In particular, subparsers, argparse.REMAINDER, and mutually exclusive groups that include both optionals and positionals are not supported.
(I posted this FAQ-style question after spending 1 hour trying to understand why the examples in the Python argparse documentation didn't seem to include it, and only by chance found a somewhat unrelated question which contained the mention to this "intermixed" function in a comment, which I am unable to find again to cite it properly.)

I'm not familiar with argparse so I would write my own code to handle arguments.
import sys
#the first argument is the name of the program, so we skip that
args = sys.argv[1:]
#just for debugging purposes:
argsLen = len(args)
print(args, argsLen)
#Create a list that will contain all of the indeces that you will have parsed through
completedIndeces = []
i = 0
#add the index to the completed indeces list.
def goToNextIndex(j):
global i
completedIndeces.append(j)
i += 1
def main():
global i
###core logic example
#Go through each item in args and decide what to do based on the arugments passed in
for argu in args:
if i in completedIndeces:
print("breaking out")
#increment i and break out of the current loop
i += 1
# If the indeces list has the index value then nothing else is done.
pass
elif argu == "joe":
print("did work with joe")
goToNextIndex(i)
elif argu == "sam":
print("did work with sam")
goToNextIndex(i)
elif argu == "school":
print("going to school")
goToNextIndex(i)
# If the argument has other arguments after it that are associated with it
# then add those indeces also to the completed indeces list.
#take in the argument following school
nextArg = i
#Do some work with the next argument
schoolName = args[nextArg]
print(f"You're going to the school called {schoolName}")
#make sure to skip the next argument as it has already been handled
completedIndeces.append(nextArg)
else:
print(f"Error the following argument is invalid: {argu}")
goToNextIndex(i)
print(f"Value of i: {i}")
print(f"completed indeces List: {completedIndeces}")
main()

Related

How do I suppress an argument when nothing is input on command line?

#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s')
parser.add_argument('--choice', '-c', default = argparse.SUPPRESS)
args = parser.parse_args()
def main(selection, choice):
print(selection)
print(choice)
if __name__=='__main__':
main(args.selection, args.choice)
The example provided is just to provide something simple and short that accurately articulates the actual problem I am facing in my project. My goal is to be able to ignore an argument within the code body when it is NOT typed into the terminal. I would like to be able to do this through passing the argument as a parameter for a function. I based my code off of searching 'suppress' in the following link: https://docs.python.org/3/library/argparse.html
When I run the code as is with the terminal input looking like so: python3 stackquestion.py -s cheese, I receive the following error on the line where the function is called:
AttributeError: 'Namespace' object has no attribute 'choice'
I've tried adding the following parameter into parser like so:
parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
I've also tried the above with
parser.add_argument('--choice', '-c')
But I get the same issue on the same line.
#Barmar answered this question in the comments. Using 'default = None' in parser.add_argument works fine; The code runs without any errors. I selected the anser from #BorrajaX because it's a simple solution to my problem.
According to the docs:
Providing default=argparse.SUPPRESS causes no attribute to be added if the command-line argument was not present:
But you're still assuming it will be there by using it in the call to main:
main(args.selection, args.choice)
A suppressed argument won't be there (i.e. there won't be an args.choice in the arguments) unless the caller specifically called your script adding --choice="something". If this doesn't happen, args.choice doesn't exist.
If you really want to use SUPPRESS, you're going to have to check whether the argument is in the args Namespace by doing if 'choice' in args: and operate accordingly.
Another option (probably more common) can be using a specific... thing (normally the value None, which is what argparse uses by default, anyway) to be used as a default, and if args.choice is None, then assume it hasn't been provided by the user.
Maybe you could look at this the other way around: You want to ensure selection is provided and leave choice as optional?
You can try to set up the arguments like this:
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s', required=True)
parser.add_argument('--choice', '-c')
args = parser.parse_args()
if __name__ == '__main__':
if args.choice is None:
print("No choice provided")
else:
print(f"Oh, the user provided choice and it's: {args.choice}")
print(f"And selection HAS TO BE THERE, right? {args.selection}")

Argparse module with multiple command and arguments

I'm trying to parse commands with arguments using the python 3 Built-in argparse module.
I have read the argparse documentation partially, however, I could not find anything that meets my requirements.
I parse the arguments as input (I have my reasons).
I have multiple commands, for each, there are both essential and optional arguments.
For example:
restart --name (the name is replaced)
restart is the command and name is the essential argument.
Currently my code would count the "--" in the input and call the function with corresponding booleans (if --all given, is_all boolean parameter will be true)
I can also add an optional argument --all (all is not replaced).
Sounds like you are looking for something like this
def get_arguments():
parser = argparse.ArgumentParser()
parser.add_argument("--arg1", required=False, default=None)
parser.add_argument("--arg2", required=False, default=None)
return parser.parse_args()
args = get_arguments()
if args.arg1:
# do something
Really hard to answer this without seeing your code or example of what you want.
I'm assuming you're doing something like a shell of sorts. I'm going to also assume that each line entered has a command, each with their own arguments.
from argparse import ArgumentParser
def get_parser(cmd):
'''Returns a parser object for a given command'''
# Instantiate the parser object, add the appropriate arguments for the command
return parser # This is an example -- you need to instantiate it
def main():
while True:
try:
in_line = input('> ')
if not in_line.strip(): # Quit if empty
break
args = in_line.split()
parser = get_parser(args[0])
opts = parser.parser_args(args)
# Do stuff with opts depending on command
except EOFError:
break
except SystemExit:
pass # Prevent failures from killing the program by trapping sys.exit()

Unexepected result for 'getopt' function

I have the following function:
def getOptions(logfile):
try:
options, arguments = getopt.getopt(programArguments[1:], 'nt:v:L:d:', ['help'])
except getopt.GetoptError:
print("\nERROR: Invalid Option")
usage()
exit()
where programArguments = sys.argv.
The getopt function is always returning nothing into options and returns a copy of programArguments[1:] into arguments. Where am I going wrong with getopt?
EDIT
See my answer below where I realised my mistake.
Are you using dashes before each argument? (e.g. python program.py -n 1)? If you were to run it like python program.py n 1 it would go into arguments.
You should only see values in arguments if you are passing values that are not assigned to a value. So for this call I get:
> python test_getopt.py --treatment=1
options: [('--treatment', '1')] arguments: []
You would then loop through the (key, value) pairs of options.
On the other hand, if I pass an additional value that is not associated with a key it will be inserted into the arguments list. For example:
> python test_getopt.py --treatment=1 temp.txt
options: [('--treatment', '1')] arguments: ['temp.txt']
See the getopt documentation for more thorough documentation.
I realised that because my program takes a file as argv[1], getopt was exiting once it found that argv[1] was not a valid option. Changing to programArguments[2:] solved the problem.

Stop parsing on first unknown argument

Using argparse, is it possible to stop parsing arguments at the first unknown argument?
I've found 2 almost solutions;
parse_known_args, but this allows for known parameters to be detected after the first unknown argument.
nargs=argparse.REMAINDER, but this won't stop parsing until the first non-option argument. Any options preceding this that aren't recognised generate an error.
Have I overlooked something? Should I be using argparse at all?
I haven't used argparse myself (need to keep my code 2.6-compatible), but looking through the docs, I don't think you've missed anything.
So I have to wonder why you want argparse to stop parsing arguments, and why the -- pseudo-argument won't do the job. From the docs:
If you have positional arguments that must begin with '-' and don’t look like negative numbers, you can insert the pseudo-argument '--' which tells parse_args() that everything after that is a positional argument:
>>> parser.parse_args(['--', '-f'])
Namespace(foo='-f', one=None)
One way to do it, although it may not be perfect in all situations, is to use getopt instead.
for example:
import sys
import os
from getopt import getopt
flags, args = getopt(sys.argv[1:], 'hk', ['help', 'key='])
for flag, v in flags:
if flag in ['-h', '--help']:
print(USAGE, file=sys.stderr)
os.exit()
elif flag in ['-k', '--key']:
key = v
Once getopt encounters a non-option argument it will stop processing arguments.

With Python's optparse module, how do you create an option that takes a variable number of arguments?

With Perl's Getopt::Long you can easily define command-line options that take a variable number of arguments:
foo.pl --files a.txt --verbose
foo.pl --files a.txt b.txt c.txt --verbose
Is there a way to do this directly with Python's optparse module? As far as I can tell, the nargs option attribute can be used to specify a fixed number of option arguments, and I have not seen other alternatives in the documentation.
This took me a little while to figure out, but you can use the callback action to your options to get this done. Checkout how I grab an arbitrary number of args to the "--file" flag in this example.
from optparse import OptionParser,
def cb(option, opt_str, value, parser):
args=[]
for arg in parser.rargs:
if arg[0] != "-":
args.append(arg)
else:
del parser.rargs[:len(args)]
break
if getattr(parser.values, option.dest):
args.extend(getattr(parser.values, option.dest))
setattr(parser.values, option.dest, args)
parser=OptionParser()
parser.add_option("-q", "--quiet",
action="store_false", dest="verbose",
help="be vewwy quiet (I'm hunting wabbits)")
parser.add_option("-f", "--filename",
action="callback", callback=cb, dest="file")
(options, args) = parser.parse_args()
print options.file
print args
Only side effect is that you get your args in a list instead of tuple. But that could be easily fixed, for my particular use case a list is desirable.
My mistake: just found this Callback Example 6.
I believe optparse does not support what you require (not directly -- as you noticed, you can do it if you're willing to do all the extra work of a callback!-). You could also do it most simply with the third-party extension argparse, which does support variable numbers of arguments (and also adds several other handy bits of functionality).
This URL documents argparse's add_argument -- passing nargs='*' lets the option take zero or more arguments, '+' lets it take one or more arguments, etc.
Wouldn't you be better off with this?
foo.pl --files a.txt,b.txt,c.txt --verbose
I recently has this issue myself: I was on Python 2.6 and needed an option to take a variable number of arguments. I tried to use Dave's solution but found that it wouldn't work without also explicitly setting nargs to 0.
def arg_list(option, opt_str, value, parser):
args = set()
for arg in parser.rargs:
if arg[0] == '-':
break
args.add(arg)
parser.rargs.pop(0)
setattr(parser.values, option.dest, args)
parser=OptionParser()
parser.disable_interspersed_args()
parser.add_option("-f", "--filename", action="callback", callback=arg_list,
dest="file", nargs=0)
(options, args) = parser.parse_args()
The problem was that, by default, a new option added by add_options is assumed to have nargs = 1 and when nargs > 0 OptionParser will pop items off rargs and assign them to value before any callbacks are called. Thus, for options that do not specify nargs, rargs will always be off by one by the time your callback is called.
This callback is can be used for any number of options, just have callback_args be the function to be called instead of setattr.
Here's one way: Take the fileLst generating string in as a string and then use http://docs.python.org/2/library/glob.html to do the expansion ugh this might not work without escaping the *
Actually, got a better way:
python myprog.py -V -l 1000 /home/dominic/radar/*.json <- If this is your command line
parser, opts = parse_args()
inFileLst = parser.largs

Categories

Resources