Argparse using optional argument with value in conjunction with positional argument - python

In my script I have, for simplicity, three arguments:
parser.add_argument("-c", "--compile")
parser.add_argument("--verbose",
help = "stores compilation results in specified log file as they come (default name: %(const)s)",
nargs = '?',
const = DEFAULT_LOG_FILE_NAME,
metavar = "LOGFILE_NAME")
parser.add_argument("path", nargs = "*")
-c and --verbose are both optional, as well as path, which is a positional argument. In addition, the argument to --verbose is also optional. If none is provided,
Say I want to combine these three in a single command.
I would run it as follows:
myscript.py -c --verbose path1 path2 path3
The problem here is that in this case, the script will interpret path1 as an argument to --verbose, unless I use --verbose=<log_name>. As far as I have been able to find, there is no way of restricting argparse to only allowing the = syntax instead of a space. I cannot count on my users understanding that either = must be used, or put --verbose as one of the last arguments.
How would I fix this? Any help is appreciated.

In this case, you are overloading --verbose to do 2 things: as an on/off flag, and as a log file option. Consider separating it into two different options: --verbose and --log=LOGFILE_NAME

Related

Argparse will not recognize arguments

This script will print env vars.
Using Python 3.9.
The goal is to able to run any subcommands if desired. The error I am getting is that if any additional short flags are added, the "ignore environment" arg is trying to parse it. I dont want this. Additional short flags go to anything assigned after --eval.
parser.py
import argparse, os
def parseargs(p):
p.usage = '%(prog)s [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]'
p.add_argument(
"-i",
"--ignore-environment",
action="store_const",
const=dict(),
dest="env",
help="start with an empty environment",
default=os.environ,
)
p.add_argument(
"--export",
nargs=1,
help="Set argument with --export NAME=VALUE"
)
p.add_argument(
"--eval",
nargs="+",
help="Run any commands with newly updated environment, "
"--eval COMMAND ARGS"
)
return p
Execution as follows
>>> p = argparse.ArgumentParser()
>>> parseargs(p) # assigns arguments to parser
>>> p.parse_args('--export FOO=bar --eval cat test.py'.split()) # This is ok and works correctly. cat is the bash command
Namespace([os.environs..], eval=['cat', 'test.py'], export=['FOO=bar'])
>>>p.parse_args('--export FOO=bar --eval ls -l'.split()) # This is fails
error: unrecognized arguments: -l
How do I get "-l" to be overlook by "-i/ignore environment" but passed to eval, like using cat test.py. I have tried using sub_parser but to no avail. The same result occurs.
The problem is that parse_args tries to identify possible options lexically before ever considering the semantics of any actual option.
Since an option taking a variable number of arguments pretty much has to be the last option used alway, consider making --eval a flag which is used to tell your program how to interpret the remaining positonal arguments. Then ls and -l can be offset by --, preventing parse_args from thinking -l is an undefined option.
p.add_argument(
"--eval",
action='store_true',
help="Run any commands with newly updated environment, "
)
# zero or more, so that you don't have to provide a dummy argument
# when the lack of --eval makes a command unnecessary.
# Wart: you can still use --eval without specifying any commands.
# I don't believe argparse alone is capable of handling this,
# at least not in a way that is simpler than just validating
# arguments after calling parse_args().
p.add_argument('cmd_and_args', nargs='*')
Then your command line could look like
>>> p.parse_args('--export FOO=bar --eval -- ls -l'.split())
or even
>>> p.parse_args('--eval --export FOO=bar -- ls -l'.split())
Later, you'll use the boolean value of args.eval to decide how to treat the list args.cmd_and_args.
Important: One wrinkle with this is that you are attaching these options to arbitrary pre-existing parsers, which may have their own positional arguments defined, so getting this to play nice with the original parser might be difficult, if not impossible.
The other option is to take a single argument to be parsed internally.
p.add_arguments("--eval")
...
args = p.parse_args()
cmd_and_args = shlex.split(args.eval) # or similar
Then
>>> p.parse_args(['--export', 'FOO=bar', '--eval', 'ls -l'])
(Note that using str.split isn't going to work for a command line like --export FOO=bar --eval "ls -l".)
From the Argparse documentation:
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 [...]
So in your case, there are no changes you can make to how you add or define the arguments, but the string you provide to be parsed should have -- preceding the arguments to the eval option, as such:
--export FOO=bar --eval ls -- -l

Python argparse has a bug (?) with single character options arguments

I have this code parsing command line arguments:
def handleCmdLineArgs(self):
parser = argparse.ArgumentParser()
parser.add_argument('-j','--juice', help='juice', default="")
parser.add_argument('-bx','--box', help='box', default="")
args,unknown = parser.parse_known_args()
When I run a command line with an argument that starts with j argparse AFTER the -j argument argparse will replace the -j argument with the remainder of the word:
Example:
program.py -j orange -jungle
argparse will return args.juice = "ungle" instead of the desired "orange"
I have created a workaround but I'm curious if anyone else has seen this or knows the reason why it is happening? Or is this maybe a bug in argparse?
This is expected behaviour. For single-dash options the space is optional. So these two are equivalent:
program.py -jorange
program.py -j orange
See the Option value syntax section of the documentation:
For short options (options only one character long), the option and its value can be concatenated:
>>> parser.parse_args(['-xX'])
Namespace(foo=None, x='X')
If you want to pass in orange -jungle as the value, you need to use quoting on the command line:
program.py -j "orange -jungle"
If you want to pass in additional positional arguments that just happen to start with a -, use -- to signal the end of the option flags:
program.py -j orange -- -jungle
See the Arguments containing - section:
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)

Argparse: Making required flags

I am attempting to create a required flag "-f" that accepts the input "filename.pdb" in Argparse.
This is simple enough. The standard solution is to add the option "required=True."
Unfortunately, after doing this, the "-f" flag still appears under optional arguments in the help list. Even more confusingly, the "-f" flag appears as required in the "usage" prompt in the help list.
Here is my code:
parser = argparse.ArgumentParser()
parser.add_argument("-f", "--file_name", required=True, help="enter name of .pdb file")
parser.add_argument("-bw", "--bin_width", default=.25, help="enter desired bin width in nanometers. default = .25")
parser.add_argument("-bn","--base_name", default="IDP", help="custom prefix for output file naming. default = IDP")
args = parser.parse_args()
And here is the help window that is returned by --help
usage: rgcalc.py [-h] -f FILE_NAME [-bw BIN_WIDTH] [-bn BASE_NAME]
optional arguments:
-h, --help show this help message and exit
-f FILE_NAME, --file_name FILE_NAME
enter name of .pdb file
-bw BIN_WIDTH, --bin_width BIN_WIDTH
enter desired bin width in nanometers. default = .25
-bn BASE_NAME, --base_name BASE_NAME
custom prefix for output file naming. default = IDP
As you can see in the "usage" block, "-f" has been taken out of brackets, indicating that it is required. Despite this, "-f" still appears in the "optional arguments" section.
Is it possible to:
A) Custom format the help window to fix this
or
B) Add some code to have the flag "-f", "--file_name" appear as a positional (opposed to an optional) argument, yet still require a flag?
I read that Argparse intentionally did this to avoid positional flags, but I am supposed to do it in order to cater to traditional linux users.
Thank you kind interwebbers!
This issue has been discussed in http://bugs.python.org/issue9694, 'argparse required arguments displayed under "optional arguments"'
It's a terminology issue that isn't easily resolved, due to historical practice (in UNIX as well as Python), and a lack of good alternatives.
Arguments that take a 'flag' like '-f' have historically been called options or optionals. Generally you don't use them unless you want some value that is different from the default. But 'argparse' lets you specify required=True, so now you have a 'required optional'. And with nargs='?', it is possible to have 'positionals' which are not required.
Until Python developers come up with some alternative terminology, your best choice is to use an 'ArgumentGroup', with the title and description that you like. By default the parser has 2 ArgumentGroups, 'optional arguments' and 'positional arguments'. It has to put the argument in one or the other. You can create others, and populate them as you wish.
see http://bugs.python.org/issue9694#msg132327 (post by the original argparse developer).
The 'usage' line is the one that accurately describes how arguments are used and whether they are required or not. 'ArgumentGroups' don't affect usage or parsing. They just determine how the help lines are grouped.
For your code:
parser = argparse.ArgumentParser()
req_grp = parser.add_argument_group(title='Required Optional')
req_grp.add_argument("-f", "--file_name", required=True, help="enter name of .pdb file")
parser.add_argument("-bw", "--bin_width", default=.25, help="enter desired bin width in nanometers. default = .25")
parser.add_argument("-bn","--base_name", default="IDP", help="custom prefix for output file naming. default = IDP")
args = parser.parse_args()
"""
usage: stack26227536.py [-h] -f FILE_NAME [-bw BIN_WIDTH] [-bn BASE_NAME]
optional arguments:
-h, --help show this help message and exit
-bw BIN_WIDTH, --bin_width BIN_WIDTH
enter desired bin width in nanometers. default = .25
-bn BASE_NAME, --base_name BASE_NAME
custom prefix for output file naming. default = IDP
Required Optional:
-f FILE_NAME, --file_name FILE_NAME
enter name of .pdb file
"""
Compare this with the help produced by dropping the -f flag:
usage: stack26227536.py [-h] [-bw BIN_WIDTH] [-bn BASE_NAME] file_name
positional arguments:
file_name enter name of .pdb file
optional arguments:
-h, --help show this help message and exit
-bw BIN_WIDTH, --bin_width BIN_WIDTH
enter desired bin width in nanometers. default = .25
-bn BASE_NAME, --base_name BASE_NAME
custom prefix for output file naming. default = IDP

Python: Allow the positional argument to be specified last or write it first in the help output when using argparse

This code
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('target', help='Specifiy who to attack!')
magic = ['fireball', 'heal', 'microwave']
parser.add_argument('-m', '--magic', nargs='*',
choices=magic,
help=('Magic'))
parsed_arguments = parser.parse_args()
Produces this help output
usage: Example.py [-h]
[-m [{fireball,heal,microwave} [{fireball,heal,microwave} ...]]]
target
positional arguments:
target Specifiy who to attack!
optional arguments:
-h, --help show this help message and exit
-m [{fireball,heal,microwave} [{fireball,heal,microwave} ...]], --magic [{fireball,heal,microwave} [{fireball,heal,microwave} ...]]
Magic
I think the help output is confusing and makes it look like the target should be specified last, which however does not work: python example.py -m fireball troll gives argument -m/--magic: invalid choice: 'troll'.
I realize the grammar of the language becomes ambiguous, but it would still be possible to tell that as there should exist one word (target) last in the sentence troll is not an argument to the -m option.
Questions:
Is there a way to let the positional argument be specified last without beating argparse to much?
Is there a way to let the argparse help output indicate that target should indeed be specified first?
As previously mentioned in a comment, the standard methods on POSIX systems to denote the end of options is to separate the arguments and options by --.
As for the second question: You might have to create your own HelpFormatter to achieve this, see formatter-class. You might be able to inherit from the default formatter and override only the necessary functions to generate the usage line.

need help for python command line argument using argparse

I need some help regarding using argparse. What I want to achieve is that I need to pass in only one argument, it could be one of the followings: --k, --r, --b, --p,(ignore the rest). If the argument count is not 1, print "usage" information and quit. Also the program needs to know which flag is passed in in order to create corresponding object. I tried several times but I doesn't work, can anyone give me a hint on this? Thanks.
What you need to use to accomplish that is a mutually exclusive group:
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument('-k', action='store_true')
group.add_argument('-r', action='store_true')
group.add_argument('-b', action='store_true')
group.add_argument('-p', action='store_true')
parser.parse_args()
As it can be seen in the example below, only one option in a mutually exclusive group is allowed at the same time:
$ python test.py -k -r -b -p
usage: test.py [-h] [-k | -r | -b | -p]
test.py: error: argument -r: not allowed with argument -k
To check which flag was passed, you just need to look at the argparse.Namespace object returned by parse_args method (the flag passed will be set to True).
How about not using argparse at all? It doesn't seem really necessary.
if len(sys.argv) != 2:
print_usage()
arg = sys.argv[1]
if arg not in ["--k", "--r", "--b", "--p"]:
print_usage()
# Do whatever you want with arg

Categories