Customizing optional arguments to include their own positional argument(s) - python

I would like to have a script which takes command-line arguments including flag options which take positional arguments themselves. I might expect the command line input to look something like
$ ./script.py [-o <file1> <file2> ...] inputfile
The official argparse documentation most similarly talks about
parser.add_argument("-v", "--verbosity", type=int, help="increase output verbosity")
args = parser.parse_args()
where the user inputs a single positional sub-argument (perhaps out of a set of choices) following the -v flag. This positional sub-argument is then stored in args.verbosity.
Thus it appears the flag's argument needs to be included in the same add_argument() line. Can you declare any special name for this sub-argument's variable (say, args.outputfile1)? Can the flag take more than one sub-argument? Can you adjust how the sub-variable looks in the help menu? By default it is something like-o OUTPUT, --output OUTPUT Save output data to a file OUTPUT. Can we change it to read -o <SomethingElse>?
Is there any more documentation which discusses this aspect?

With a definition like:
parser.add_argument('-o','--output', dest='output_file_name', nargs='+')
you can give a commandline like:
$ ./script.py -o file1 file2
and get args.output_file_name equal to ['file1','file2']. The '+' means one or more arguments (other nargs values are documented).
But
$ ./script.py -o file1 file2 an_input_file
where 'an_input_file' goes to a positional argument is harder to achieve. '*' is greedy, taking everything, leaving nothing for the positional. It's better to define another optional
parser.add_argument('-i','--input')
$ ./script.py -o file1 file2 -i an_input_file
If the '-o' is defined as an 'append' Action, you can use:
$ ./script.py -o file1 -o file2 -i an_input_file
In general you get the best control by using optionals. Positionals, because they 'parse' by position, not value, are harder to use in fancy combinations.
The metavar parameter lets you change the help display.

Your proposed interface is pretty unusual. It's more common to have people specify the same option multiple times, because tt's hard to distinguish ./script.py [-o file1 file2 ...] input file from ./script.py [-o file1] file2 inputfile. That might not be an issue yet, but as you add options or god-forbid arguments, your unusual design will become an issue.
I would recommend doing one of the following solutions:
1. Repeat the option flag
./script.py -o file1 -o file2 inputfile
2. Make your option a boolean flag
Change your API so -o indicates that all arguments except the last one are output files:
./script.py -o output1 output2 ... inputfileislast

Related

How to declare an optional argument to the parser which disables compulsory argument?

The title may be confusing but I can't think of a better explanation. Basically, I have a program which operates on some input file with a bunch of optional arguments. The input file is compulsory for my program. So I wrote a parser like this:
test.py
from argparse import ArgumentParser
parser = ArgumentParser(prog="prog")
parser.add_argument("file", help="input file")
parser.add_argument("--debug", action="store_true", help="enable debug messages")
parser.parse_args()
I'm not showing all the options for simplicity. Now, I want to add a feature to this program which don't require input file and runs on its own. Let's call this option as a. Option a takes unknown number of arguments and --debug or other options are not valid for option a. So the following would be valid runs for my program:
$ python3 test.py input.txt
$ python3 test.py input.txt --debug
$ python3 test.py -a 1 2 3
And these would be invalid:
$ python3 test.py input.txt -a 1 2 3
$ python3 test.py -a 1 2 3 --debug
When I add this line to my code
parser.add_argument("-a", nargs="+", help="help option a")
It accepts option a but it says "file is required" for understandable reasons. So how should I modify my code to achieve my goal?
P.S: I also tried using subparser but I couldn't manage to make it work as I want probably because my lack of knowledge.

Multiple positional and optional arguments using docopt

I'm trying to implement a python3 CLI using the docopt package. I'm trying to get my program accept multiple positional input files and, optionally a list of output files.
A MWE for my docstring is:
__doc__ = """
Usage:
test.py [INPUT...] [-o OUTPUT...] [-t TEST]
Options:
-o OUTPUT..., --output OUTPUT... #One output file for each INPUT file [default: DEFAULT]
-t TEST, --test TEST #A test option
"""
For example a programm call as
test.py FILE_A FILE_B -o OUTFILE_A OUTFILE B -t true
Should return a dict:
{'--output': ['OUTFILE_A', 'OUTFILE_B'],
'--test': 'true',
'INPUT': ['FILE_A', 'FILE_B']}
but for some reason it is always appended to the INPUT arguments:
{'--output': ['OUTFILE_A'],
'--test': 'true',
'INPUT': ['FILE_A', 'FILE_B', 'OUTFILE_B']}
Options in Docopt unfortunately can only take one argument, so [-o OUTPUT...] will not work. The remaining elements will as you state be interpreted as additional arguments.
One way around this is to move the ellipsis outside the square brackets:
Usage:
test.py [INPUT...] [-o OUTPUT]... [-t TEST]
and use it like this:
test.py FILE_A FILE_B -o OUTFILE_A -o OUTFILE_B
It doesn't look as good, but it works.

accessing multiple files using argparse

I'm trying to read number of files using argparse:
parser.add_argument(
'-f',
'--text-file',
metavar='IN FILE',
type=argparse.FileType('r'),
nargs='*')
...
...
args = parser.parse_args()
print args
when more than one files are passed as command line arguments, only last file appears into args:
python example.py -o x.xml -s sss -c ccc -t "hello world" --report_failure -f ex.1 -f ex.2
Namespace(outputfile=<open file 'x.xml', mode 'w' at 0x028AD4F0>, report_failure=True, test_case='ccc', test_suite='sss', text='hello world', text_file=[<open file 'ex.2', mode 'r' at 0x028AD5A0>])
What I did wrong and how to access all files I passed from the command line?
Note: I'm using python 2.7.6 on Windows.
The complication occurs because you're passing multiple arguments to the same parameter -f and each argument replaces the argument before it. What would work in this case is:
python example.py -o x.xml -s sss -c ccc -t "hello world"
--report_failure -f ex.1 ex.2
This will collect ex.1 and ex.2 into a list, which is what I assume you want to do.
as a reference here is the docs on nargs:
'*'. All command-line arguments present are gathered into a list. Note
that it generally doesn’t make much sense to have more than one
positional argument with nargs='', but multiple optional arguments
with nargs='' is possible.
https://docs.python.org/3/library/argparse.html#nargs

argparse: optional argument as flag and variable

I'm writing a program that takes two argument (paths) and as an option -l, witch enables save the log in a log file.
I'm using argparse and create the following argument:
parser.add_argument('-l', '--logfile',
nargs='?',
const='tranfer.log',
default=False,
help='save log of the transfer')
These are the forms of use:
prog.py path_1 path_2. Nothing special.
prog.py -l filename path_1 path_2. Save the log in filename file.
prog.py -l path_1 path_2. Save the log the file by default (say, logfile).
I have a problem in item 3, because prog.py takes path_1 as the filename of the log file. The issue was easily fixed puttin -l option at the end of the line.
prog.py path_1 path_2 -l
But I was wondering if there's a way of tell argparse to use the last two option as the path files, because I'll be not the only one who use the program.
Path argument were add as follows:
parser.add_argument('left_path',
action='store',
help='first path')
parser.add_argument('right_path',
action='store',
help='second path')
Your assessment is right,
prog.py -l path_1 path_2
will assign path_1 to l and path_2 to the first positional, and raise an error about missing 2nd positional.
http://bugs.python.org/issue9338 argparse optionals with nargs='?', '*' or '+' can't be followed by positionals is a bug/issue that deals with this. Patches have been proposed, but not implemented. It's not trivial. When handing -l the parser would have to look ahead to see how many arguments are needed to satisfy the positionals, and refrain from consuming the next string (even though by your definition it has every right to do so).
It's also been discussed in previous SO questions.
https://stackoverflow.com/a/29853186/901925
https://stackoverflow.com/a/26986546/901925
You have to either put the optional last, or use some other means of signaling the end of its list of arguments (-- or other flagged optional). Or change the argument definitions, so that -l is not ? (or the equivalent), or change the positionals to flagged.
The neat way to do this would be to introduce option flags for the both the paths arguments too. Then there would be no ambiguity and you'd be able to say:
prog.py -l -leftpath path_1 -rightpath path_2
you can also store all options in a single argument and check by hand, as in
parser.add_argument('-p', dest='path', nargs='?',
default=('path1/','path2/'))
args = parser.parse_args()
if len(args.path) == 3:
args.logfile = args.path[0]
args.path = args.path[1:]
elif len(args.path) == 2:
args.logfile = ''
else:
print 'Error'
but then you have to have the -p flag if you want to set the paths to be different from the defaults.

argparse, two arguments depend on each other

I would like to make the parser like cmd [-a xxx -b xxx] -c xxx -d xxx
When -a is used, I want -b to be used too. likewise, if -b is used, -a must be used too. It's ok both -a and -b are not used.
How do I do that? I have tried custom actions, but it does not go well.
A better design would be to have a single option that takes two arguments:
parser.add_argument('-a', nargs=2)
Then you either specify the option with 2 arguments, or you don't specify it at all.
$ script -a 1 2
or
$ script
A custom action (or postprocessing) can split the tuple args.a into two separate values args.a and args.b.
Argparse doesn't natively support this type of use.
The most effective thing to do is check and see if those types of conditions are met after parsing:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-b')
parser.add_argument('-c')
args = parser.parse_args()
required_together = ('b','c')
# args.b will be None if b is not provided
if not all([getattr(args,x) for x in required_together]):
raise RuntimeError("Cannot supply -c without -b")

Categories

Resources