Question

I have the following code (using Python 2.7):

# shared command line options, like --version or --verbose
parser_shared = argparse.ArgumentParser(add_help=False)
parser_shared.add_argument('--version', action='store_true')

# the main parser, inherits from `parser_shared`
parser = argparse.ArgumentParser(description='main', parents=[parser_shared])

# several subcommands, which can't inherit from the main parser, since
# it would expect subcommands ad infinitum
subparsers = parser.add_subparsers('db', parents=[parser_shared])

...

args = parser.parse_args()

Now I would like to be able to call this program e.g. with the --version appended to the normal program or some subcommand:

$ prog --version
0.1

$ prog db --version
0.1

Basically, I need to declare optional subparsers. I'm aware that this isn't really supported, but are there any workarounds or alternatives?

Edit: The error message I am getting:

$ prog db --version
# works fine

$ prog --version
usage: ....
prog: error: too few arguments
Was it helpful?

Solution

According to documentation, --version with action='version' (and not with action='store_true') prints automatically the version number:

parser.add_argument('--version', action='version', version='%(prog)s 2.0')

OTHER TIPS

FWIW, I ran into this also, and ended up "solving" it by not using subparsers (I already had my own system for printing help, so didn't lose anything there).

Instead, I do this:

parser.add_argument("command", nargs="?",
                    help="name of command to execute")

args, subcommand_args = parser.parse_known_args()

...and then the subcommand creates its own parser (similar to a subparser) which operates only on subcommand_args.

This seems to implement the basic idea of an optional subparser. We parse the standard arguments that apply to all subcommands. Then, if anything is left, we invoke the parser on the rest. The primary arguments are a parent of the subcommand so the -h appears correctly. I plan to enter an interactive prompt if no subcommands are present.

import argparse

p1 = argparse.ArgumentParser( add_help = False )    
p1.add_argument( ‘–flag1′ )

p2 = argparse.ArgumentParser( parents = [ p1 ] )
s = p2.add_subparsers()
p = s.add_parser( ‘group’ )
p.set_defaults( group=True )

( init_ns, remaining ) = p1.parse_known_args( )

if remaining:
    p2.parse_args( args = remaining, namespace=init_ns )
else:
    print( ‘Enter interactive loop’ )

print( init_ns )

As discussed in http://bugs.python.org/issue9253 (argparse: optional subparsers), as of Python 3.3, subparsers are now optional. This was an unintended result of a change in how parse_args checked for required arguments.

I found a fudge that restores the previous (required subparsers) behavior, explicitly setting the required attribute of the subparsers action.

parser = ArgumentParser(prog='test')
subparsers = parser.add_subparsers()
subparsers.required = True   # the fudge
subparsers.dest = 'command'
subparser = subparsers.add_parser("foo", help="run foo")
parser.parse_args()

See that issue for more details. I expect that if and when this issue gets properly patched, subparsers will be required by default, with some sort of option to set its required attribute to False. But there is a big backlog of argparse patches.

Yeah, I just checked svn, which is used as an object example in the add_subparsers() documentation, and it only supports '--version' on the main command:

python zacharyyoung$ svn log --version
Subcommand 'log' doesn't accept option '--version'
Type 'svn help log' for usage.

Still:

# create common parser
parent_parser = argparse.ArgumentParser('parent', add_help=False)
parent_parser.add_argument('--version', action='version', version='%(prog)s 2.0')

# create the top-level parser
parser = argparse.ArgumentParser(parents=[parent_parser])
subparsers = parser.add_subparsers()

# create the parser for the "foo" command
parser_foo = subparsers.add_parser('foo', parents=[parent_parser])

Which yields:

python zacharyyoung$ ./arg-test.py --version
arg-test.py 2.0
python zacharyyoung$ ./arg-test.py foo --version
arg-test.py foo 2.0

While we wait for this feature to be delivered, we can use code like this:

# Make sure that main is the default sub-parser
if '-h' not in sys.argv and '--help' not in sys.argv:
    if len(sys.argv) < 2:
        sys.argv.append('main')
    if sys.argv[1] not in ('main', 'test'):
        sys.argv = [sys.argv[0], 'main'] + sys.argv[1:]

Although @eumiro's answer address the --version option, it can only do so because that is a special case for optparse. To allow general invocations of:

 prog
 prog --verbose
 prog --verbose main
 prog --verbose db 

and have prog --version work the same as prog --verbose main (and prog main --verbose) you can add a method to Argumentparser and call that with the name of the default subparser, just before invoking parse_args():

import argparse
import sys

def set_default_subparser(self, name, args=None):
    """default subparser selection. Call after setup, just before parse_args()
    name: is the name of the subparser to call by default
    args: if set is the argument list handed to parse_args()

    , tested with 2.7, 3.2, 3.3, 3.4
    it works with 2.6 assuming argparse is installed
    """
    subparser_found = False
    for arg in sys.argv[1:]:
        if arg in ['-h', '--help']:  # global help if no subparser
            break
    else:
        for x in self._subparsers._actions:
            if not isinstance(x, argparse._SubParsersAction):
                continue
            for sp_name in x._name_parser_map.keys():
                if sp_name in sys.argv[1:]:
                    subparser_found = True
        if not subparser_found:
            # insert default in first position, this implies no
            # global options without a sub_parsers specified
            if args is None:
                sys.argv.insert(1, name)
            else:
                args.insert(0, name)

argparse.ArgumentParser.set_default_subparser = set_default_subparser

def do_main(args):
    print 'main verbose', args.verbose

def do_db(args):
    print 'db verbose:', args.verbose

parser = argparse.ArgumentParser()
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--version', action='version', version='%(prog)s 2.0')
subparsers = parser.add_subparsers()
sp = subparsers.add_parser('main')
sp.set_defaults(func=do_main)
sp.add_argument('--verbose', action='store_true')
sp = subparsers.add_parser('db')
sp.set_defaults(func=do_db)

parser.set_default_subparser('main')
args = parser.parse_args()

if hasattr(args, 'func'):
    args.func(args)

The set_default_subparser() method is part of the ruamel.std.argparse package.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top