I have a need in Python to create a list of arguments dynamically. I've created a script to demonstrate this, named args.py, shown below:
#!/usr/bin/python
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-args_file', default = 'args.txt')
with open(parser.parse_args().args_file, 'r') as f:
args = f.readlines()
for arg in args:
parser.add_argument('-' + arg.strip())
dynamic_args = parser.parse_args()
print dynamic_args
I also have a text file in the same folder, named args.txt, also shown below:
arg1
arg2
arg3
As expected, running args.py with no arguments results in:
Namespace(arg1=None, arg2=None, arg3=None, args_file='args.txt')
However, what I'm having trouble with is running my script with the -h argument. I would like the help to display the arguments found in the args_file, as seen in the example below:
usage: args.py [-h] [-args_file ARGS_FILE] [-arg1 ARG1] [-arg2 ARG2]
[-arg3 ARG3]
What I'm seeing instead is:
usage: args.py [-h] [-args_file ARGS_FILE]
Moreover, if I run the script interactively (i.e. python -i arg.py), and at the interactive prompt type the command "parser.print_usage()", I get the wanted response (showing the -argN arguments). Also, typing "arg.py -arg1 1" or "arg.py arg1 1" result in "unrecognized arguments".
I've tried everything I can think of, but I've been unsuccessful thus far. Do any of the Python aficionados have any suggestions?
As Martjin pointed out, you can omit the help from the parser the first time. The other thing to do is to use parse_known_args the first time, so you only parse the args_file.
In cases like this, I like to keep things clear by using a throwaway parser for the first parse, and a full parser for the final parse:
import argparse
argfile_parser = argparse.ArgumentParser(add_help=False)
full_parser = argparse.ArgumentParser()
argfile_parser.add_argument('-args_file', default = 'args.txt')
full_parser.add_argument('-args_file', default = 'args.txt')
with open(argfile_parser.parse_known_args()[0].args_file, 'r') as f:
for arg in f:
full_parser.add_argument('-' + arg.strip())
dynamic_args = full_parser.parse_args()
print dynamic_args
For testing, I added a file args2.txt:
argA
argB
argC
And I think the result is what you're looking for:
lap:~$ python tmp.py -h
usage: tmp.py [-h] [-args_file ARGS_FILE] [-arg1 ARG1] [-arg2 ARG2]
[-arg3 ARG3]
optional arguments:
-h, --help show this help message and exit
-args_file ARGS_FILE
-arg1 ARG1
-arg2 ARG2
-arg3 ARG3
lap:~$ python tmp.py -args_file args2.txt
Namespace(argA=None, argB=None, argC=None, args_file='args2.txt')
lap:~$ python tmp.py -h -args_file args2.txt
usage: tmp.py [-h] [-args_file ARGS_FILE] [-argA ARGA] [-argB ARGB]
[-argC ARGC]
optional arguments:
-h, --help show this help message and exit
-args_file ARGS_FILE
-argA ARGA
-argB ARGB
-argC ARGC
lap:~$ python tmp.py -arg1 foo
Namespace(arg1='foo', arg2=None, arg3=None, args_file='args.txt')
lap:~$ python tmp.py -args_file args2.txt -argA bar
Namespace(argA='bar', argB=None, argC=None, args_file='args2.txt')
From Hazen's answer, using parents option makes it easier.
https://docs.python.org/3/library/argparse.html#parents
import argparse
argfile_parser = argparse.ArgumentParser(add_help=False)
argfile_parser.add_argument('-args_file', default = 'args.txt')
full_parser = argparse.ArgumentParser(parents=[argfile_parser])
with open(argfile_parser.parse_known_args()[0].args_file, 'r') as f:
for arg in f:
full_parser.add_argument('-' + arg.strip())
dynamic_args = full_parser.parse_args()
print dynamic_args
Related
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 3 years ago.
Improve this question
I'm trying to create a python script that'll accept some arguments from the command line. I'm trying to use argparse but can't get it to work properly.
I need it to work similar to the way the aws cli works e.g. aws s3 cp has it's own arguments, aws s3 ls, has it's own etc
ref: https://docs.aws.amazon.com/cli/latest/reference/s3/cp.html
https://docs.aws.amazon.com/cli/latest/reference/s3/ls.html
This is what I have but it always needs the mycmd option
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("mycmd", help="my test cmd")
parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true")
args = parser.parse_args()
if args.mycmd:
print(f"arg is mycmd")
if args.verbose:
print("args v")
End result should be that mycmd1 has arguments xyz, mycmd2 has arguments abc etc and both can be ran from a single python file e.g. python3 somename.py mycmd1 -x ...
You could use the Python Click package, which has explicit support for subcommands:
import click
#click.group()
def cli():
pass
#cli.command()
#click.option('--arg1')
def mycmd1(arg1):
click.echo('My command 1')
if arg1:
click.echo(arg1)
#cli.command()
#click.option('--arg2')
def mycmd2(arg2):
click.echo('My command 2')
if arg2:
click.echo(arg2)
if __name__ == '__main__':
cli()
Usage:
(~)$ python -m click-example --help
Usage: click-example.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
mycmd1
mycmd2
(~)$ python -m click-example mycmd1 --help
Usage: click-example.py mycmd1 [OPTIONS]
Options:
--arg1 TEXT
--help Show this message and exit.
(~)$ python -m click-example mycmd2 --help
Usage: click-example.py mycmd2 [OPTIONS]
Options:
--arg2 TEXT
--help Show this message and exit.
(~)$ python -m click-example mycmd2 --arg1 err
Usage: click-example.py mycmd2 [OPTIONS]
Try "click-example.py mycmd2 --help" for help.
Error: no such option: --arg1
(~)$ python -m click-example mycmd1 --arg1 hello
My command 1
hello
(~)$
I think your code is completely valid. "mycmd" is just the name of your argument. I made some changes to your code to make it clearer:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("mycmd", help="my test cmd")
parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true")
args = parser.parse_args()
if args.mycmd == "mycmd1":
print(f"arg is mycmd1")
elif args.mycmd == "mycmd2":
print(f"arg is mycmd2")
else:
print("arg not allowed")
if args.verbose:
print("args v")
On my machine:
$ python3 test.py mycmd1 -v
arg is mycmd1
args v
$ python3 test.py 12 -v
arg not allowed
args v
$ python3 test.py mycmd2 -v
arg is mycmd2
args v
As per argparse documentation, if you do not state whether a positional argument is optional or not, it will be always needed.
You can state how many times you want your option to be used by nargs. In your case, add nargs='?' to make it optional (interpreted like in regex, ? means "0 or 1").
See also nargs in argparse's documentation - there are nice examples of optional input/output files positional arguments
In your case, you might find useful parent arg parsers. Link to the part of the documentation about parents - just remember to read the part about handling colliding arguments (by default, both parsers will have -h option but you also might need some adjustments with your own arguments).
c.py
import argparse
import sys
parser = argparse.ArgumentParser(
prog='c', description="Description: to do some task",
epilog='Run c.py --help for more information')
subparser = parser.add_subparsers(title="Commands", help="commands")
mycmd_args = subparser.add_parser('mycmd', help='to dome some task',description="To do some task")
mycmd_args.add_argument("--arg1", "-a1", dest="argument1",help="provide argument")
mycmd_args.add_argument("--arg2", "-a2", dest="argument2",help="provide argument")
mycmd1_args = subparser.add_parser('mycmd1', help='to dome some task',description="To do some task")
mycmd1_args.add_argument("--arg1", "-a1", dest="argument1",help="provide argument")
mycmd1_args.add_argument("--arg2", "-a2", dest="argument2",help="provide argument")
if __name__=="__main__":
args=parser.parse_args(sys.argv[1:])
if len(sys.argv) <= 1:
sys.argv.append("-h")
elif sys.argv[1] == "mycmd":
print("mycmd arguemnts")
print(args.argument1)
print(args.argument2)
elif sys.argv[1] == "mycmd1":
print("mycmd1 arguemnts")
print(args.argument1)
print(args.argument2)
else:
sys.argv.append("-h")
output:
C:\Users\jt250054\Desktop>python c.py --help
usage: c [-h] {mycmd,mycmd1} ...
Description: to do some task
optional arguments:
-h, --help show this help message and exit
Commands:
{mycmd,mycmd1} commands
mycmd to dome some task
mycmd1 to dome some task
Run c.py --help for more information
C:\Users\jt250054\Desktop>python c.py mycmd --help
usage: c mycmd [-h] [--arg1 ARGUMENT1] [--arg2 ARGUMENT2]
To do some task
optional arguments:
-h, --help show this help message and exit
--arg1 ARGUMENT1, -a1 ARGUMENT1
provide argument
--arg2 ARGUMENT2, -a2 ARGUMENT2
provide argument
C:\Users\jt250054\Desktop>python c.py mycmd --arg1 argument1
mycmd arguemnts
argument1
None
C:\Users\jt250054\Desktop>python c.py mycmd --arg1 argument1 --arg2 arugment2
mycmd arguemnts
argument1
arugment2
C:\Users\jt250054\Desktop>
I'm developing a command line tool with Python whose functionality is broken down into a number of sub-commands, and basically each one takes as arguments input and output files. The tricky part is that each command requires different number of parameters (some require no output file, some require several input files, etc).
Ideally, the interface would be called as:
./test.py ncinfo inputfile
Then, the parser would realise that the ncinfo command requires a single argument (if this does not fit the input command, it complains), and then it calls the function:
ncinfo(inputfile)
that does the actual job.
When the command requires more options, for instance
./test.py timmean inputfile outputfile
the parser would realise it, check that indeed the two arguments are given, and then then it calls:
timmean(inputfile, outputfile)
This scheme is ideally generalised for an arbitrary list of 1-argument commands, 2-argument commands, and so on.
However I'm struggling to get this behaviour with Python argparse. This is what I have so far:
#! /home/navarro/SOFTWARE/anadonda3/bin/python
import argparse
# create the top-level parser
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
# create the parser for the "ncinfo" command
parser_1 = subparsers.add_parser('ncinfo', help='prints out basic netCDF strcuture')
parser_1.add_argument('filein', help='the input file')
# create the parser for the "timmean" command
parser_2 = subparsers.add_parser('timmean', help='calculates temporal mean and stores it in output file')
parser_2.add_argument('filein', help='the input file')
parser_2.add_argument('fileout', help='the output file')
# parse the argument lists
parser.parse_args()
print(parser.filein)
print(parser.fileout)
But this doesn't work as expected. First, when I call the script without arguments, I get no error message telling me which options I have. Second, when I try to run the program to use ncinfo, I get an error
./test.py ncinfo testfile
Traceback (most recent call last):
File "./test.py", line 21, in <module>
print(parser.filein)
AttributeError: 'ArgumentParser' object has no attribute 'filein'
What am I doing wrong that precludes me achieving the desired behaviour? Is the use of subparsers sensible in this context?
Bonus point: is there a way to generalise the definition of the commands, so that I do not need to add manually every single command? For instance, grouping all 1-argument commands into a list, and then define the parser within a loop. This sounds reasonable, but I don't know if it is possible. Otherwise, as the number of tools grows, the parser itself is going to become hard to maintain.
import argparse
import sys
SUB_COMMANDS = [
"ncinfo",
"timmean"
]
def ncinfo(args):
print("executing: ncinfo")
print(" inputfile: %s" % args.inputfile)
def timmean(args):
print("executing: timmean")
print(" inputfile: %s" % args.inputfile)
print(" outputfile: %s" % args.outputfile)
def add_parser(subcmd, subparsers):
if subcmd == "ncinfo":
parser = subparsers.add_parser("ncinfo")
parser.add_argument("inputfile", metavar="INPUT")
parser.set_defaults(func=ncinfo)
elif subcmd == "timmean":
parser = subparsers.add_parser("timmean")
parser.add_argument("inputfile", metavar="INPUT")
parser.add_argument("outputfile", metavar="OUTPUT")
parser.set_defaults(func=timmean)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-o', '--common-option', action='store_true')
subparsers = parser.add_subparsers(help="sub-commands")
for cmd in SUB_COMMANDS:
add_parser(cmd, subparsers)
args = parser.parse_args(sys.argv[1:])
if args.common_option:
print("common option is active")
try:
args.func(args)
except AttributeError:
parser.error("too few arguments")
Some usage examples:
$ python test.py --help
usage: test.py [-h] [-o] {ncinfo,timmean} ...
positional arguments:
{ncinfo,timmean} sub-commands
optional arguments:
-h, --help show this help message and exit
-o, --common-option
$ python test.py ncinfo --help
usage: test.py ncinfo [-h] INPUT
positional arguments:
INPUT
optional arguments:
-h, --help show this help message and exit
$ python test.py timmean --help
usage: test.py timmean [-h] INPUT OUTPUT
positional arguments:
INPUT
OUTPUT
optional arguments:
-h, --help show this help message and exit
$ python test.py -o ncinfo foo
common option is active
executing: ncinfo
inputfile: foo
$ python test.py -o timmean foo bar
common option is active
executing: timmean
inputfile: foo
outputfile: bar
Can I use argparse to read named command line arguments that do not need to be in a specific order? I browsed through the documentation but most of it focused on displaying content based on the arguments provided (such as --h).
Right now, my script reads ordered, unnamed arguments:
myscript.py foo-val bar-val
using sys.argv:
foo = sys.argv[1]
bar = sys.argv[2]
But I would like to change the input so that it is order agnostic by naming arguments:
myscript.py --bar=bar-val --foo=foo-val
You can use the Optional Arguments like so.
With this program:
#!/usr/bin/env python3
import argparse, sys
parser=argparse.ArgumentParser()
parser.add_argument("--bar", help="Do the bar option")
parser.add_argument("--foo", help="Foo the program")
args=parser.parse_args()
print(f"Args: {args}\nCommand Line: {sys.argv}\nfoo: {args.foo}")
print(f"Dict format: {vars(args)}")
Make it executable:
$ chmod +x prog.py
Then if you call it with:
$ ./prog.py --bar=bar-val --foo foo-val
It prints:
Args: Namespace(bar='bar-val', foo='foo-val')
Command Line: ['./prog.py', '--bar=bar-val', '--foo', 'foo-val']
foo: foo-val
Dict format: {'bar': 'bar-val', 'foo': 'foo-val'}
Or, if the user wants help argparse builds that too:
$ ./prog.py -h
usage: prog.py [-h] [--bar BAR] [--foo FOO]
options:
-h, --help show this help message and exit
--bar BAR Do the bar option
--foo FOO Foo the program
2022-08-30: Updated to Python3 this answer...
The answer is yes. A quick look at the argparse documentation would have answered as well.
Here is a very simple example, argparse is able to handle far more specific needs.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--foo', '-f', help="a random options", type= str)
parser.add_argument('--bar', '-b', help="a more random option", type= int, default= 0)
print(parser.format_help())
# usage: test_args_4.py [-h] [--foo FOO] [--bar BAR]
#
# optional arguments:
# -h, --help show this help message and exit
# --foo FOO, -f FOO a random options
# --bar BAR, -b BAR a more random option
args = parser.parse_args("--foo pouet".split())
print(args) # Namespace(bar=0, foo='pouet')
print(args.foo) # pouet
print(args.bar) # 0
Off course, in a real script, you won't hard-code the command-line options and will call parser.parse_args() (without argument) instead. It will make argparse take the sys.args list as command-line arguments.
You will be able to call this script this way:
test_args_4.py -h # prints the help message
test_args_4.py -f pouet # foo="pouet", bar=0 (default value)
test_args_4.py -b 42 # foo=None, bar=42
test_args_4.py -b 77 -f knock # foo="knock", bar=77
You will discover a lot of other features by reading the doc ;)
I think it might help you with a simple one
#! /usr/bin/python3
import sys
keys = ["--paramkey=","-p="]
for i in range(1,len(sys.argv)):
for key in keys:
if sys.argv[i].find(key) == 0:
print(f"The Given value is: {sys.argv[i][len(key):]}")
break
Run:
$ ./example.py --paramkey=paramvalue -p=pvalue
Output:
The Given value is: paramvalue
The Given value is: pvalue
I have a Python program that takes as its (only) positional command-line argument one or more file path expressions. I'm using argparse for the CL parsing, and argparse.REMAINDER for the variable that contains the file path(s). See code below:
import argparse
import sys
# Create parser
parser = argparse.ArgumentParser(
description="My test program")
def parseCommandLine():
# Add arguments
parser.add_argument('filesIn',
action="store",
type=str,
nargs=argparse.REMAINDER,
help="input file(s)")
# Parse arguments
args = parser.parse_args()
return(args)
def main():
# Get input from command line
args = parseCommandLine()
# Input files
filesIn = args.filesIn
# Print help message and exit if filesIn is empty
if len(filesIn) == 0:
parser.print_help()
sys.exit()
# Do something
print(filesIn)
if __name__ == "__main__":
main()
Now, when a user runs the script without any arguments, this results in the following help message:
usage: test.py [-h] ...
Where ... represents the positional input. From a user's perspective it would be more informative if the name of the variable (filesIn) was displayed here instead. Especially because typing test.py -h results in this:
usage: test.py [-h] ...
My test program
positional arguments:
filesIn input file(s)
I.e. the usage line displays ... but then in the list of positional arguments filesIn is used.
So my question is whether there's some easy way to change this (i.e. always display filesIn)?
Don't use argparse.REMAINDER here. You are not gathering all remaining arguments, you are trying to take filenames.
Use '+' instead to capture all remaining arguments as filenames and you need at least one:
parser.add_argument('filesIn',
action="store",
type=str,
nargs='+',
help="input file(s)")
This produces better help output:
$ bin/python test.py
usage: test.py [-h] filesIn [filesIn ...]
test.py: error: too few arguments
$ bin/python test.py -h
usage: test.py [-h] filesIn [filesIn ...]
My test program
positional arguments:
filesIn input file(s)
optional arguments:
-h, --help show this help message and exit
I'm using the Python argparse module for command line subcommands in my program. My code basically looks like this:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title="subcommands", metavar="<command>")
subparser = subparsers.add_parser("this", help="do this")
subparser = subparsers.add_parser("that", help="do that")
parser.parse_args()
When running "python test.py --help" I would like to list the available subcommands. Currently I get this output:
usage: test.py [-h] <command> ...
optional arguments:
-h, --help show this help message and exit
subcommands:
<command>
this do this
that do that
Can I somehow remove the <command> line in the subcommands listing and still keep it in the usage line? I have tried to give help=argparse.SUPPRESS as argument to add_subparsers, but that just hides all the subcommands in the help output.
I solved it by adding a new HelpFormatter that just removes the line if formatting a PARSER action:
class SubcommandHelpFormatter(argparse.RawDescriptionHelpFormatter):
def _format_action(self, action):
parts = super(argparse.RawDescriptionHelpFormatter, self)._format_action(action)
if action.nargs == argparse.PARSER:
parts = "\n".join(parts.split("\n")[1:])
return parts