Passing in multiple options for argparse in Python - python

I have been looking at argparse documentation but I am still confused how to use it.
I made a python script to get issues from either pmd, checkstyle, or findbugs after a code analysis. Theses issues are also categorized into severities such as major, blocker, and critical.
So I want to be able to pass in two arguments in the form python script.py arg1 arg2 where arg1 would be a combination of p,c,f which stands for pmd, checkstyle, or findbug and arg2 would be a combination of m,c,b which stands for major, critical, and blocker.
So for instance, if I write python script.py pf cb in the terminal, I would get pmd and findbugs issues of critical and blocker severity.
It would be awesome if someone can give me a general structure of how this should go.
Thanks.

Perhaps you'd rather let the user specify the flags more verbose, like this?
python script.py --checker p --checker f --level c --level b
If so, you can use append
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--checker', action='append')
>>> parser.add_argument('--level', action='append')
Then you get a parameter checker and level, both as lists to iterate over.
If you really want to use the combined flags:
for c in arg1:
run_checker(c, arg2)
Assuming that you just pass the severity levels to the checker in some way.

You could try setting boolean flags if each option is present in an argument:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("pcf", help="pmd, checkstyle, or findbug")
parser.add_argument("mcb", help="major, critical, and blocker")
args = parser.parse_args()
# Use boolean flags
pmd = 'p' in args.pcf
checkstyle = 'c' in args.pcf
findbug = 'f' in args.pcf
major = 'm' in args.mcb
critical = 'c' in args.mcb
blocker = 'b' in args.mcb
This would work using python script.py pf cb.
Also, just a helpful hint. If you place the following at the top of your python file and then make it executable with chmod +x script.py you can then call the file directly (using a *NIX operating system):
#!/usr/bin/env python
import argparse
...
Now run with ./script.py pf cb or even put it in your path and call script.py pf cb

https://stackoverflow.com/users/1401034/ewan is a good solution given your specification. But a slight variation gives users more options, and might be clearer.
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--pmd', action='store_true')
parser.add_argument('-c', '--checkstyle', action='store_true')
parser.add_argument('-f', '--findbug', action='store_true')
parser.add_argument('-m', '--major', action='store_true')
parser.add_argument('-d', '--critical', action='store_true')
parser.add_argument('-b', '--blocker', action='store_true')
args = parser.parse_args()
print args
Sample runs:
1840:~/mypy$ python2.7 stack24265375.py -cfmd
Namespace(blocker=False, checkstyle=True, critical=True, findbug=True, major=True, pmd=False)
1841:~/mypy$ python2.7 stack24265375.py -h
usage: stack24265375.py [-h] [-p] [-c] [-f] [-m] [-d] [-b]
optional arguments:
-h, --help show this help message and exit
-p, --pmd
-c, --checkstyle
-f, --findbug
-m, --major
-d, --critical
-b, --blocker
1842:~/mypy$ python2.7 stack24265375.py --critical --major
Namespace(blocker=False, checkstyle=False, critical=True, findbug=False, major=True, pmd=False)
In this case, there are 6 optional, boolean flags. The short, single letter versions can be strung together in any combination. Except the necessary '-', this form can look just like the one using 2 positional arguments. But they can be mixed and matched in any combination. And the long form is self documenting.
The help display might be clearer if these 6 arguments were divided into 2 argument groups.
parser = argparse.ArgumentParser()
group1 = parser.add_argument_group('issue')
group1.add_argument('-p', '--pmd', action='store_true')
group1.add_argument('-c', '--checkstyle', action='store_true')
group1.add_argument('-f', '--findbug', action='store_true')
group2 = parser.add_argument_group('severity')
group2.add_argument('-m', '--major', action='store_true')
group2.add_argument('-d', '--critical', action='store_true')
group2.add_argument('-b', '--blocker', action='store_true')
args = parser.parse_args()
print args
with the help:
usage: stack24265375.py [-h] [-p] [-c] [-f] [-m] [-d] [-b]
optional arguments:
-h, --help show this help message and exit
issue:
-p, --pmd
-c, --checkstyle
-f, --findbug
severity:
-m, --major
-d, --critical
-b, --blocker

Related

argparse claims argument althoug default value specified [duplicate]

I have a script which is meant to be used like this:
usage: installer.py dir [-h] [-v]
dir is a positional argument which is defined like this:
parser.add_argument('dir', default=os.getcwd())
I want the dir to be optional: when it's not specified it should just be cwd.
Unfortunately when I don't specify the dir argument, I get Error: Too few arguments.
Use nargs='?' (or nargs='*' if you need more than one dir)
parser.add_argument('dir', nargs='?', default=os.getcwd())
extended example:
>>> import os, argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('-v', action='store_true')
_StoreTrueAction(option_strings=['-v'], dest='v', nargs=0, const=True, default=False, type=None, choices=None, help=None, metavar=None)
>>> parser.add_argument('dir', nargs='?', default=os.getcwd())
_StoreAction(option_strings=[], dest='dir', nargs='?', const=None, default='/home/vinay', type=None, choices=None, help=None, metavar=None)
>>> parser.parse_args('somedir -v'.split())
Namespace(dir='somedir', v=True)
>>> parser.parse_args('-v'.split())
Namespace(dir='/home/vinay', v=True)
>>> parser.parse_args(''.split())
Namespace(dir='/home/vinay', v=False)
>>> parser.parse_args(['somedir'])
Namespace(dir='somedir', v=False)
>>> parser.parse_args('somedir -h -v'.split())
usage: [-h] [-v] [dir]
positional arguments:
dir
optional arguments:
-h, --help show this help message and exit
-v
As an extension to #VinaySajip answer. There are additional nargs worth mentioning.
parser.add_argument('dir', nargs=1, default=os.getcwd())
N (an integer). N arguments from the command line will be gathered together into a list
parser.add_argument('dir', nargs='*', default=os.getcwd())
'*'. 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.
parser.add_argument('dir', nargs='+', default=os.getcwd())
'+'. Just like '*', all command-line args present are gathered into a list. Additionally, an error message will be generated if there wasn’t at least one command-line argument present.
parser.add_argument('dir', nargs=argparse.REMAINDER, default=os.getcwd())
argparse.REMAINDER. All the remaining command-line arguments are gathered into a list. This is commonly useful for command line utilities that dispatch to other command line utilities
If the nargs keyword argument is not provided, the number of arguments consumed is determined by the action. Generally this means a single command-line argument will be consumed and a single item (not a list) will be produced.
Edit (copied from a comment by #Acumenus) nargs='?' The docs say: '?'. One argument will be consumed from the command line if possible and produced as a single item. If no command-line argument is present, the value from default will be produced.
Short Answer
As already shown in the previous two answers, you can accept an optional positional argument with nargs='?'. You could also turn the argument directly into a Path type and/or shorten the cwd to . if you wanted to:
myfile.py
import argparse
import pathlib
parser = argparse.ArgumentParser()
parser.add_argument("dir", nargs="?", default=".", type=pathlib.Path)
parsed_args = parser.parse_args()
print("Installing to", parsed_args.dir.resolve())
$ python myfile.py
Installing to /users/myname/myfolder
$ python myfile.py /usr/bin/
Installing to /usr/bin
Longer answer
Since you also mention the flag-style True/False options -h and -v in your question, these examples may be of use:
Flags (e.g. -v)
We might refer to optional options that take no arguments as "flags". With flags, we only care about whether they are given or not. -h is a flag that argparse adds automatically (along with the longer version --help) so we shouldn't really override that. If we consider -v then,
myfile.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"-v",
"--version",
action="store_true")
parsed_args = parser.parse_args()
if parsed_args.version:
print("version flag given")
else:
print("version flag not given")
Note that the second argument to add_argument() is a longer name for the option. It is not mandatory but it does make your subsequent code more readable (parsed_args.version vs parsed_args.v) and makes calls to your installer more explicit.
$ python myfile.py -v
version flag given
$ python myfile.py --version
version flag given
$ python myfile.py
version flag not given
Optional arguments (e.g. --installdir /usr/bin/)
One could argue that, in your case, you would be better off with an optional argument rather than a positional one.
myfile.py
import argparse
import pathlib
parser = argparse.ArgumentParser()
parser.add_argument(
"-i",
"--installdir", # Optional (but recommended) long version
type=pathlib.Path,
default="/bin"
)
parsed_args = parser.parse_args()
print("Installing to", parsed_args.installdir)
$ python myfile.py -i /usr/bin/
Installing to /usr/bin
$ python myfile.py --installdir /usr/bin/
Installing to /usr/bin
$ python myfile.py
Installing to /bin
parser.add_argument also has a switch required. You can use required=False.
Here is a sample snippet with Python 2.7:
parser = argparse.ArgumentParser(description='get dir')
parser.add_argument('--dir', type=str, help='dir', default=os.getcwd(), required=False)
args = parser.parse_args()

How to define a mutually exclusive group of two positional arguments?

I would like to use argparse to make some code to be used in the following two ways:
./tester.py all
./tester.py name someprocess
i.e. either all is specified OR name with some additional string.
I have tried to implement as follows:
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument('all', action='store_true', \
help = "Stops all processes")
group.add_argument('name', \
help = "Stops the named process")
print parser.parse_args()
which gives me an error
ValueError: mutually exclusive arguments must be optional
Any idea how to do it right? I also would like to avoid sub parsers in this case.
The question is a year old, but since all the answers suggest a different syntax, I'll give something closer to the OP.
First, the problems with the OP code:
A positional store_true does not make sense (even if it is allowed). It requires no arguments, so it is always True. Giving an 'all' will produce error: unrecognized arguments: all.
The other argument takes one value and assigns it to the name attribute. It does not accept an additional process value.
Regarding the mutually_exclusive_group. That error message is raised even before parse_args. For such a group to make sense, all the alternatives have to be optional. That means either having a -- flag, or be a postional with nargs equal to ? or *. And doesn't make sense to have more than one such positional in the group.
The simplest alternative to using --all and --name, would be something like this:
p=argparse.ArgumentParser()
p.add_argument('mode', choices=['all','name'])
p.add_argument('process',nargs='?')
def foo(args):
if args.mode == 'all' and args.process:
pass # can ignore the process value or raise a error
if args.mode == 'name' and args.process is None:
p.error('name mode requires a process')
args = p.parse_args()
foo(args) # now test the namespace for correct `process` argument.
Accepted namespaces would look like:
Namespace(mode='name', process='process1')
Namespace(mode='all', process=None)
choices imitates the behavior of a subparsers argument. Doing your own tests after parse_args is often simpler than making argparse do something special.
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-a','--all', action='store_true', \
help = "Stops all processes")
group.add_argument('-n','--name', \
help = "Stops the named process")
print parser.parse_args()
./tester.py -h
usage: zx.py [-h] (-a | -n NAME)
optional arguments:
-h, --help show this help message and exit
-a, --all Stops all processes
-n NAME, --name NAME Stops the named process
"OR name with some additional string."
positional argument cannot take additional string
I think the best solution for you is (named test.py):
import argparse
p = argparse.ArgumentParser()
meg = p.add_mutually_exclusive_group()
meg.add_argument('-a', '--all', action='store_true', default=None)
meg.add_argument('-n', '--name', nargs='+')
print p.parse_args([])
print p.parse_args(['-a'])
print p.parse_args('--name process'.split())
print p.parse_args('--name process1 process2'.split())
print p.parse_args('--all --name process1'.split())
$ python test.py
Namespace(all=None, name=None)
Namespace(all=True, name=None)
Namespace(all=None, name=['process'])
Namespace(all=None, name=['process1', 'process2'])
usage: t2.py [-h] [-a | -n NAME [NAME ...]]
t2.py: error: argument -n/--name: not allowed with argument -a/--all
I would agree that this looks exactly like a sub-parser problem, and that if you don't want to make it an optional argument by using --all and --name, one suggestion from me would be just to ignore the all and name altogether, and use the following semantics:
If tester.py is called without any arguments, stop all process.
If tester.py is called with some arguments, stop only those processes.
Which can be done using:
import argparse, sys
parser = argparse.ArgumentParser()
parser.add_argument('processes', nargs='*')
parsed = parser.parse(sys.argv[1:])
print parsed
which will behave as follows:
$ python tester.py
Namespace(processes=[])
$ python tester.py proc1
Namespace(processes=['proc1'])
Or, if you insist on your own syntax, you can create a custom class. And actually you're not having a "mutually exclusive group" case, since I assume if all is specified, you will ignore the rest of the arguments (even when name is one of the other arguments), and when name is specified, anything else after that will be regarded as processes' name.
import argparse
import sys
class AllOrName(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if len(values)==0:
raise argparse.ArgumentError(self, 'too few arguments')
if values[0]=='all':
setattr(namespace, 'all', True)
elif values[0]=='name':
if len(values)==1:
raise argparse.ArgumentError(self, 'please specify at least one process name')
setattr(namespace, 'name', values[1:])
else:
raise argparse.ArgumentError(self, 'only "all" or "name" should be specified')
parser = argparse.ArgumentParser()
parser.add_argument('processes', nargs='*', action=AllOrName)
parsed = parser.parse_args(sys.argv[1:])
print parsed
with the following behaviour:
$ python argparse_test.py name
usage: argparse_test.py [-h] [processes [processes ...]]
argparse_test.py: error: argument processes: please specify at least one process name
$ python argparse_test.py name proc1
Namespace(name=['proc1'], processes=None)
$ python argparse_test.py all
Namespace(all=True, processes=None)
$ python argparse_test.py host
usage: argparse_test.py [-h] [processes [processes ...]]
argparse_test.py: error: argument processes: only "all" or "name" should be specified
$ python argparse_test.py
usage: argparse_test.py [-h] [processes [processes ...]]
argparse_test.py: error: argument processes: too few arguments
This is probably what you're looking for:
group.add_argument('--all', dest=is_all, action='store_true')
group.add_argument('--name', dest=names, nargs='+')
Passing --name will then require at list one value and store them as a list.

In python, how to get subparsers to read in parent parser's argument?

Here is an example code:
import argparse
parser=argparse.ArgumentParser()
parser.add_argument('-main_arg')
subparser=parser.add_subparser()
a=subparser.add_parser('run')
a.add_argument('required_sub_arg')
a.add_argument('arg_a')
b=subparser.add_parser('b')
parser.parse_args()
I want it to read in -main_arg if I enter program run required_sub_arg -main_arg -arg_a
Right now, it doesn't recognize -main_arg as a valid argument.
PSA to recent readers
As this question still has visits in 2018, before doing anything this complex with argparse, please consider using docopt or click instead. It will improve both your sanity and that of anyone who might read or modify your code. Thank you.
Original answer
As is, you have a few issues.
First, parser.parse_args is a method that returns a namespace of parser's arguments, so you should do something like
args = parser.parse_args()
Then args.main_args to get-main_arg from a call like
program -main_arg run required_sub_arg -arg_a
Your issue with main_arg is that you have created a argument to parser named main_arg, and you make a call like
program run required_sub_arg -main_arg -arg_a
that refers to an argument to a named main_arg. Since a doesn't have such an argument, it is invalid.
In order to refer to a parser's argument from one of its subparser, you have to make said subparser inherit the arguments of its parent. This is done with
a=parser.add_subparser('run', parents=[parser])
You have mistaken subparser for child parser. See http://docs.python.org/dev/py3k/library/argparse.html and https://code.google.com/p/argparse/issues/detail?id=54 for more informations.
For anyone else using argparse that arrives here looking for a way to display "common" sub-parser arguments in the "main" help screen, here's one approach:
import argparse
common = argparse.ArgumentParser(add_help=False)
common.add_argument('--shared', action='store_true', help='some shared arg')
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--parent', action='store_true', help='parent only arg')
subparsers = parser.add_subparsers()
run = subparsers.add_parser('run', parents=[common])
run.add_argument('--fast', action='store_true', help='run only arg')
parser.epilog = "--- Arguments common to all sub-parsers ---" \
+ common.format_help().replace(common.format_usage(), '')
args = parser.parse_args()
Main help:
$ program.py -h
usage: program.py [-h] {run} ...
positional arguments:
{run}
optional arguments:
-h, --help show this help message and exit
--parent parent only arg
--- Arguments common to all sub-parsers ---
optional arguments:
--shared some shared arg
run sub-parser help:
$ program.py run -h
usage: program.py run [-h] [--shared]
optional arguments:
-h, --help show this help message and exit
--shared some shared arg
--fast run only arg
To address the actual question, since the accepted answer doesn't run for me, here's some additional information on why it doesn't seem possible to truly share argparse arguments with the same name across both parent and child/sub-parser parsers.
First, the problem with the following code:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-main_arg')
subparsers = parser.add_subparsers()
run = subparsers.add_parser('run', parents=[parser])
args = parser.parse_args()
Is that it leads to the following error, because both parent parser and sub-parser run define the -h/--help argument (by default).
Argparse.ArgumentError: argument -h/--help: conflicting option strings: -h, --help
While this error can be avoided by suppressing the -h/--help option (with add_help=False) on either the parent or the child, it's nice to have the help option at both levels.
Another potential way to avoid conflicting help options is to move common arguments to a shared parser, common:
import argparse
common = argparse.ArgumentParser(add_help=False)
common.add_argument('-main_arg', action='store_true')
parser = argparse.ArgumentParser(parents=[common])
subparsers = parser.add_subparsers()
run = subparsers.add_parser('run', parents=[common])
args = parser.parse_args()
print(args)
While this appears to work on the surface, in practice, it doesn't work as intended:
$ program.py run # OK
Namespace(main_arg=False)
$ program.py run -main_arg # OK
Namespace(main_arg=True)
$ program.py -main_arg run # BAD: expected main_arg to be True
Namespace(main_arg=False)
The behavior observed when parsing program.py -main_arg run illustrates a key relationship: a parent argparser and its sub-parsers are independent parsers, where the parent parses all arguments up to the sub-parser "command" positional argument, and then the selected sub-parser parses the remaining arguments in the same Namespace as the parent with no regard for attributes that may have been set by the parent.

Python argparse: Make at least one argument required

I've been using argparse for a Python program that can -process, -upload or both:
parser = argparse.ArgumentParser(description='Log archiver arguments.')
parser.add_argument('-process', action='store_true')
parser.add_argument('-upload', action='store_true')
args = parser.parse_args()
The program is meaningless without at least one parameter. How can I configure argparse to force at least one parameter to be chosen?
UPDATE:
Following the comments: What's the Pythonic way to parametrize a program with at least one option?
if not (args.process or args.upload):
parser.error('No action requested, add -process or -upload')
args = vars(parser.parse_args())
if not any(args.values()):
parser.error('No arguments provided.')
I know this is old as dirt, but the way to require one option but forbid more than one (XOR) is like this:
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-process', action='store_true')
group.add_argument('-upload', action='store_true')
args = parser.parse_args()
print args
Output:
>opt.py
usage: multiplot.py [-h] (-process | -upload)
multiplot.py: error: one of the arguments -process -upload is required
>opt.py -upload
Namespace(process=False, upload=True)
>opt.py -process
Namespace(process=True, upload=False)
>opt.py -upload -process
usage: multiplot.py [-h] (-process | -upload)
multiplot.py: error: argument -process: not allowed with argument -upload
If not the 'or both' part (I have initially missed this) you could use something like this:
parser = argparse.ArgumentParser(description='Log archiver arguments.')
parser.add_argument('--process', action='store_const', const='process', dest='mode')
parser.add_argument('--upload', action='store_const', const='upload', dest='mode')
args = parser.parse_args()
if not args.mode:
parser.error("One of --process or --upload must be given")
Though, probably it would be a better idea to use subcommands instead.
Requirements Review
use argparse (I will ignore this one)
allow one or two actions to be called (at least one required).
try to by Pythonic (I would rather call it "POSIX"-like)
There are also some implicit requirements when living on command line:
explain the usage to the user in a way which is easy to understand
options shall be optional
allow specifying flags and options
allow combining with other parameters (like file name or names).
Sample solution using docopt (file managelog.py):
"""Manage logfiles
Usage:
managelog.py [options] process -- <logfile>...
managelog.py [options] upload -- <logfile>...
managelog.py [options] process upload -- <logfile>...
managelog.py -h
Options:
-V, --verbose Be verbose
-U, --user <user> Username
-P, --pswd <pswd> Password
Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>
"""
if __name__ == "__main__":
from docopt import docopt
args = docopt(__doc__)
print args
Try to run it:
$ python managelog.py
Usage:
managelog.py [options] process -- <logfile>...
managelog.py [options] upload -- <logfile>...
managelog.py [options] process upload -- <logfile>...
managelog.py -h
Show the help:
$ python managelog.py -h
Manage logfiles
Usage:
managelog.py [options] process -- <logfile>...
managelog.py [options] upload -- <logfile>...
managelog.py [options] process upload -- <logfile>...
managelog.py -h
Options:
-V, --verbose Be verbose
-U, --user <user> Username
-P, --pswd <pswd> P managelog.py [options] upload -- <logfile>...
Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>
And use it:
$ python managelog.py -V -U user -P secret upload -- alfa.log beta.log
{'--': True,
'--pswd': 'secret',
'--user': 'user',
'--verbose': True,
'-h': False,
'<logfile>': ['alfa.log', 'beta.log'],
'process': False,
'upload': True}
Short alternative short.py
There can be even shorter variant:
"""Manage logfiles
Usage:
short.py [options] (process|upload)... -- <logfile>...
short.py -h
Options:
-V, --verbose Be verbose
-U, --user <user> Username
-P, --pswd <pswd> Password
Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>
"""
if __name__ == "__main__":
from docopt import docopt
args = docopt(__doc__)
print args
Usage looks like this:
$ python short.py -V process upload -- alfa.log beta.log
{'--': True,
'--pswd': None,
'--user': None,
'--verbose': True,
'-h': False,
'<logfile>': ['alfa.log', 'beta.log'],
'process': 1,
'upload': 1}
Note, that instead of boolean values for "process" and "upload" keys there are counters.
It turns out, we cannot prevent duplication of these words:
$ python short.py -V process process upload -- alfa.log beta.log
{'--': True,
'--pswd': None,
'--user': None,
'--verbose': True,
'-h': False,
'<logfile>': ['alfa.log', 'beta.log'],
'process': 2,
'upload': 1}
Conclusions
Designing good command line interface can be challenging sometime.
There are multiple aspects of command line based program:
good design of command line
selecting/using proper parser
argparse offers a lot, but restricts possible scenarios and can become very complex.
With docopt things go much shorter while preserving readability and offering high degree of flexibility. If you manage getting parsed arguments from dictionary and do some of conversions (to integer, opening files..) manually (or by other library called schema), you may find docopt good fit for command line parsing.
For http://bugs.python.org/issue11588 I am exploring ways of generalizing the mutually_exclusive_group concept to handle cases like this.
With this development argparse.py, https://github.com/hpaulj/argparse_issues/blob/nested/argparse.py
I am able to write:
parser = argparse.ArgumentParser(prog='PROG',
description='Log archiver arguments.')
group = parser.add_usage_group(kind='any', required=True,
title='possible actions (at least one is required)')
group.add_argument('-p', '--process', action='store_true')
group.add_argument('-u', '--upload', action='store_true')
args = parser.parse_args()
print(args)
which produces the following help:
usage: PROG [-h] (-p | -u)
Log archiver arguments.
optional arguments:
-h, --help show this help message and exit
possible actions (at least one is required):
-p, --process
-u, --upload
This accepts inputs like '-u', '-up', '--proc --up' etc.
It ends up running a test similar to https://stackoverflow.com/a/6723066/901925, though the error message needs to be clearer:
usage: PROG [-h] (-p | -u)
PROG: error: some of the arguments process upload is required
I wonder:
are the parameters kind='any', required=True clear enough (accept any of the group; at least one is required)?
is usage (-p | -u) clear? A required mutually_exclusive_group produces the same thing. Is there some alternative notation?
is using a group like this more intuitive than phihag's simple test?
The best way to do this is by using python inbuilt module add_mutually_exclusive_group.
parser = argparse.ArgumentParser(description='Log archiver arguments.')
group = parser.add_mutually_exclusive_group()
group.add_argument('-process', action='store_true')
group.add_argument('-upload', action='store_true')
args = parser.parse_args()
If you want only one argument to be selected by command line just use required=True as an argument for group
group = parser.add_mutually_exclusive_group(required=True)
If you require a python program to run with at least one parameter, add an argument that doesn't have the option prefix (- or -- by default) and set nargs=+ (Minimum of one argument required). The problem with this method I found is that if you do not specify the argument, argparse will generate a "too few arguments" error and not print out the help menu. If you don't need that functionality, here's how to do it in code:
import argparse
parser = argparse.ArgumentParser(description='Your program description')
parser.add_argument('command', nargs="+", help='describe what a command is')
args = parser.parse_args()
I think that when you add an argument with the option prefixes, nargs governs the entire argument parser and not just the option. (What I mean is, if you have an --option flag with nargs="+", then --option flag expects at least one argument. If you have option with nargs="+", it expects at least one argument overall.)
This achieves the purpose and this will also be relfected in the argparse autogenerated --help output, which is imho what most sane programmers want (also works with optional arguments):
parser.add_argument(
'commands',
nargs='+', # require at least 1
choices=['process', 'upload'], # restrict the choice
help='commands to execute'
)
Official docs on this:
https://docs.python.org/3/library/argparse.html#choices
Maybe use sub-parsers?
import argparse
parser = argparse.ArgumentParser(description='Log archiver arguments.')
subparsers = parser.add_subparsers(dest='subparser_name', help='sub-command help')
parser_process = subparsers.add_parser('process', help='Process logs')
parser_upload = subparsers.add_parser('upload', help='Upload logs')
args = parser.parse_args()
print("Subparser: ", args.subparser_name)
Now --help shows:
$ python /tmp/aaa.py --help
usage: aaa.py [-h] {process,upload} ...
Log archiver arguments.
positional arguments:
{process,upload} sub-command help
process Process logs
upload Upload logs
optional arguments:
-h, --help show this help message and exit
$ python /tmp/aaa.py
usage: aaa.py [-h] {process,upload} ...
aaa.py: error: too few arguments
$ python3 /tmp/aaa.py upload
Subparser: upload
You can add additional options to these sub-parsers as well. Also instead of using that dest='subparser_name' you can also bind functions to be directly called on given sub-command (see docs).
For cases like
parser.add_argument("--a")
parser.add_argument("--b")
We can use the following
if not args.a and not args.b:
parser.error("One of --a or --b must be present")
Use append_const to a list of actions and then check that the list is populated:
parser.add_argument('-process', dest=actions, const="process", action='append_const')
parser.add_argument('-upload', dest=actions, const="upload", action='append_const')
args = parser.parse_args()
if(args.actions == None):
parser.error('Error: No actions requested')
You can even specify the methods directly within the constants.
def upload:
...
parser.add_argument('-upload', dest=actions, const=upload, action='append_const')
args = parser.parse_args()
if(args.actions == None):
parser.error('Error: No actions requested')
else:
for action in args.actions:
action()
Using
parser = argparse.ArgumentParser(description='Log archiver arguments.')
parser.add_argument('-process', action='store_true')
parser.add_argument('-upload', action='store_true')
args = parser.parse_args()
Maybe try:
if len([False for arg in vars(args) if vars(args)[arg]]) == 0:
parsers.print_help()
exit(-1)
At least this is what I just used; hopefully this helps someone in the future!

Only one command line option with argparse

I'm trying to create a CLI with the argparse module but I'd like to have different commands with different argument requirements, I tried this:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('foo', help='foo help')
parser.add_argument('test', nargs=1, help='test help')
args = parser.parse_args()
what I'd like is to be able to run python test.py foo and python test.py test somearg
but when I run python test.py foo I get error: too few arguments. Is there a way that the commands could behave like git status, git commit or pip install? or is there a better way to create a CLI in python?
#crodjer is correct;
to provide an example:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title='subcommands',
description='valid subcommands',
help='additional help')
foo_parser = subparsers.add_parser('foo', help='foo help')
bar_parser = subparsers.add_parser('bar', help='bar help')
bar_parser.add_argument('somearg')
args = parser.parse_args()
Test of different args per subparser:
$ python subparsers_example.py bar somearg
Namespace(somearg='somearg')
$ python subparsers_example.py foo
Namespace()
$ python subparsers_example.py foo somearg
usage: argparse_subparsers.py foo [-h]
subparser_example.py foo: error: unrecognized arguments: somearg
Help output:
$ python subparsers_example.py foo -h
usage: argparse_subparsers.py foo [-h]
optional arguments:
-h, --help show this help message and exit
$ python subparsers_example.py bar -h
usage: argparse_subparsers.py bar [-h] somearg
positional arguments:
somearg
optional arguments:
-h, --help show this help message and exit
This is what you probably want:
http://docs.python.org/library/argparse.html#sub-commands
With this you can add sub arguments which have their own argument schemes.
By default, argparse arguments consume one value. If you want foo to have different behavior, you'll need to specify it. It looks like you think the default is nargs=0, but it's not. From the argparse documentation (at http://docs.python.org/dev/library/argparse.html#nargs): "If the nargs keyword argument is not provided, the number of args consumed is determined by the action. Generally this means a single command-line arg will be consumed and a single item (not a list) will be produced."
You can either use nargs='?' for foo and give it a default value in case nothing is provided from the command-line, or use a non-default action (perhaps 'store_true'?).

Categories