Default sub-command, or handling no sub-command with argparse

Viewed 16863

How can I have a default sub-command, or handle the case where no sub-command is given using argparse?

import argparse

a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')
a.parse_args()

Here I'd like a command to be selected, or the arguments to be handled based only on the next highest level of parser (in this case the top-level parser).

joiner@X:~/src> python3 default_subcommand.py
usage: default_subcommand.py [-h] {hi} ...
default_subcommand.py: error: too few arguments
10 Answers

You can duplicate the default action of a specific subparser on the main parser, effectively making it the default.

import argparse
p = argparse.ArgumentParser()
sp = p.add_subparsers()

a = sp.add_parser('a')
a.set_defaults(func=do_a)

b = sp.add_parser('b')
b.set_defaults(func=do_b)

p.set_defaults(func=do_b)
args = p.parse_args()

if args.func:
    args.func()
else:
    parser.print_help()

Does not work with add_subparsers(required=True), which is why the if args.func is down there.

In my case I found it easiest to explicitly provide the subcommand name to parse_args() when argv was empty.

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='commands')

runParser = subparsers.add_parser('run', help='[DEFAULT ACTION]')

altParser = subparsers.add_parser('alt', help='Alternate command')
altParser.add_argument('alt_val', type=str, help='Value required for alt command.')

# Here's my shortcut: If `argv` only contains the script name,
# manually inject our "default" command.
args = parser.parse_args(['run'] if len(sys.argv) == 1 else None)
print args

Example runs:

$ ./test.py 
Namespace()

$ ./test.py alt blah
Namespace(alt_val='blah')

$ ./test.py blah
usage: test.py [-h] {run,alt} ...
test.py: error: invalid choice: 'blah' (choose from 'run', 'alt')

In python 2.7, you can override the error behaviour in a subclass (a shame there isn't a nicer way to differentiate the error):

import argparse

class ExceptionArgParser(argparse.ArgumentParser):

    def error(self, message):
        if "invalid choice" in message:
            # throw exception (of your choice) to catch
            raise RuntimeError(message)
        else:
            # restore normal behaviour
            super(ExceptionArgParser, self).error(message)


parser = ExceptionArgParser()
subparsers = parser.add_subparsers(title='Modes', dest='mode')

default_parser = subparsers.add_parser('default')
default_parser.add_argument('a', nargs="+")

other_parser = subparsers.add_parser('other')
other_parser.add_argument('b', nargs="+")

try:
    args = parser.parse_args()
except RuntimeError:
    args = default_parser.parse_args()
    # force the mode into namespace
    setattr(args, 'mode', 'default') 

print args

Here's another solution using a helper function to build a list of known subcommands:

import argparse


def parse_args(argv):
    parser = argparse.ArgumentParser()

    commands = []
    subparsers = parser.add_subparsers(dest='command')

    def add_command(name, *args, **kwargs):
        commands.append(name)
        return subparsers.add_parser(name, *args, **kwargs)

    hi = add_command("hi")
    hi.add_argument('--name')
    add_command("hola")

    # check for default command
    if not argv or argv[0] not in commands:
        argv.insert(0, "hi")

    return parser.parse_args(argv)


assert parse_args([]).command == 'hi'
assert parse_args(['hi']).command == 'hi'
assert parse_args(['hi', '--name', 'John']).command == 'hi'
assert parse_args(['hi', '--name', 'John']).name == 'John'
assert parse_args(['--name', 'John']).command == 'hi'
assert parse_args(['hola']).command == 'hola'
Related