118

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?

tim-kt
  • 304
  • 5
  • 17
Adam Matan
  • 128,757
  • 147
  • 397
  • 562
  • 10
    `-x` is universally a flag and optional. Cut the `-` if it's required. –  Jul 17 '11 at 09:27
  • 1
    Couldn't you make `process` the default behavior (without the need to specify any options) and allow the user to change that into `upload` if *that* option is set? Usually, options should be optional, hence the name. Required options should be avoided (this is also in the `argparse` docs). – Tim Pietzcker Jul 17 '11 at 09:32
  • @AdamMatan It is almost three years since you have asked your question but I liked the challenge hidden in it and used the advantage of new solutions being available for this kind of tasks. – Jan Vlcinsky Jun 09 '14 at 21:21

13 Answers13

142
if not (args.process or args.upload):
    parser.error('No action requested, add -process or -upload')
phihag
  • 278,196
  • 72
  • 453
  • 469
47

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  
Knut
  • 625
  • 6
  • 7
  • 10
    Unfortunately, the OP doesn't want an XOR. It's either or both, but not none so your last test case doesn't meet their requirements. – kdopen Oct 09 '15 at 22:55
  • 3
    @kdopen: the respondent did clarify that this is a variation on the original question, which I found to be useful: "the way to require one option but forbid more than one " Perhaps the etiquette of Stack Exchange would call for a new question instead. But having this answer present here helped me... – erik.weathers Dec 20 '15 at 00:24
  • 2
    I will second the usefulness of this answer, this ended up being exactly what I was looking for. – Mark Edington Nov 08 '17 at 19:42
  • 3
    This post doesn't answer the initial question – Marc Jan 16 '18 at 09:14
  • 1
    At least that was exactly what I was searching for so thanks a lot. – T.Nel Jul 02 '18 at 09:00
  • 2
    How does this answer the question of "at least one"? – xaxxon Jul 22 '19 at 19:44
  • 2
    Unfortunately, the OP doesn't want an XOR. – iamthedrake Oct 01 '19 at 17:30
  • 1
    I guess I came here based on a slightly different question. But it turns out I was asking the wrong question. This answer is exactly what I need. Thank you sir. – Chris Wong Oct 19 '21 at 21:07
  • It looks like the returned type of `add_mutually_exclusive_group()`, `_MutuallyExclusiveGroup`, in turn, supports `add_argument_group()` and `add_mutually_exclusive_group()`. Maybe there could be *groups* inside of a mutually exclusive group? This might solve the OP's question. – Victor Sergienko Jul 22 '22 at 00:30
45
args = vars(parser.parse_args())
if not any(args.values()):
    parser.error('No arguments provided.')
brentlance
  • 2,189
  • 1
  • 19
  • 25
21

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.

jdotjdot
  • 16,134
  • 13
  • 66
  • 118
Jacek Konieczny
  • 8,283
  • 2
  • 23
  • 35
  • 4
    I think he wants to allow `--process` OR `--upload`, not XOR. This prevents both options from being set at the same time. – phihag Jul 17 '11 at 09:54
  • +1 because you mentioned subcommands. Yet - as somebody pointed in the comments `-x` and `--xxx` are typically optional parameters. – mac Jul 17 '11 at 09:56
11

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.

Jan Vlcinsky
  • 42,725
  • 12
  • 101
  • 98
7

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?

Community
  • 1
  • 1
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • I can't find any mention of `add_usage_group` on this page: https://docs.python.org/2/library/argparse.html; will you please provide a link to the documentation for it? – Myer May 14 '18 at 09:25
  • @P.MyerNore, I did provide a link - at the start of this answer. This has not been put into production. – hpaulj May 14 '18 at 15:31
7

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)
faizan baig
  • 1,283
  • 15
  • 21
5

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.)

NuclearPeon
  • 5,743
  • 4
  • 44
  • 52
5

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

Bob
  • 5,809
  • 5
  • 36
  • 53
3

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).

jhutar
  • 1,369
  • 2
  • 17
  • 32
2

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")
Kaushal Banthia
  • 117
  • 1
  • 3
1

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()
storm_m2138
  • 2,281
  • 2
  • 20
  • 18
0

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!

Red
  • 1