Default sub-command, or handling no sub-command with argparse
Asked Answered
A

10

47

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
Atlantean answered 15/6, 2011 at 23:24 Comment(0)
A
8

It seems I've stumbled on the solution eventually myself.

If the command is optional, then this makes the command an option. In my original parser configuration, I had a package command that could take a range of possible steps, or it would perform all steps if none was given. This makes the step a choice:

parser = argparse.ArgumentParser()

command_parser = subparsers.add_parser('command')
command_parser.add_argument('--step', choices=['prepare', 'configure', 'compile', 'stage', 'package'])

...other command parsers

parsed_args = parser.parse_args()

if parsed_args.step is None:
    do all the steps...
Atlantean answered 18/6, 2011 at 9:9 Comment(5)
However this means if different subcommands have different flags you have to handle that logic yourself rather than letting argparse take care of it.Prefabricate
Don't understand what you mean by this, the code doesn't look any different. Did you remove "required" somewhere?Comport
What I mean is that because the command "step", is optional above, it is better treated as a flag.Atlantean
A more elegant way would be to use the dest value. No need to rely on the arguments. docsMarleen
"If the command is optional, then this makes the command an option." This is not always true. For example, git remote by default acts as git remote show, but show is not an option, but a subcommand (like add, rm, etc). However, to simulate that I manually checked the arguments, as you propose.Howler
S
22

On Python 3.2 (and 2.7) you will get that error, but not on 3.3 and 3.4 (no response). Therefore on 3.3/3.4 you could test for parsed_args to be an empty Namespace.

A more general solution is to add a method set_default_subparser() (taken from the ruamel.std.argparse package) and call that method just before parse_args():

import argparse
import sys

def set_default_subparser(self, name, args=None, positional_args=0):
    """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 last position before global positional
            # arguments, this implies no global options are specified after
            # first positional argument
            if args is None:
                sys.argv.insert(len(sys.argv) - positional_args, name)
            else:
                args.insert(len(args) - positional_args, name)

argparse.ArgumentParser.set_default_subparser = set_default_subparser

def do_hi():
    print('inside hi')

a = argparse.ArgumentParser()
b = a.add_subparsers()
sp = b.add_parser('hi')
sp.set_defaults(func=do_hi)

a.set_default_subparser('hi')
parsed_args = a.parse_args()

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

This will work with 2.6 (if argparse is installed from PyPI), 2.7, 3.2, 3.3, 3.4. And allows you to do both

python3 default_subcommand.py

and

python3 default_subcommand.py hi

with the same effect.

Allowing to chose a new subparser for default, instead of one of the existing ones.

The first version of the code allows setting one of the previously-defined subparsers as a default one. The following modification allows adding a new default subparser, which could then be used to specifically process the case when no subparser was selected by user (different lines marked in the code)

def set_default_subparser(self, name, args=None, positional_args=0):
    """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
    existing_default = False # check if default parser previously defined
    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 sp_name == name: # check existance of default parser
                    existing_default = True
        if not subparser_found:
            # If the default subparser is not among the existing ones,
            # create a new parser.
            # As this is called just before 'parse_args', the default
            # parser created here will not pollute the help output.

            if not existing_default:
                for x in self._subparsers._actions:
                    if not isinstance(x, argparse._SubParsersAction):
                        continue
                    x.add_parser(name)
                    break # this works OK, but should I check further?

            # insert default in last position before global positional
            # arguments, this implies no global options are specified after
            # first positional argument
            if args is None:
                sys.argv.insert(len(sys.argv) - positional_args, name)
            else:
                args.insert(len(args) - positional_args, name)

argparse.ArgumentParser.set_default_subparser = set_default_subparser

a = argparse.ArgumentParser()
b = a.add_subparsers(dest ='cmd')
sp = b.add_parser('hi')
sp2 = b.add_parser('hai')

a.set_default_subparser('hey')
parsed_args = a.parse_args()

print(parsed_args)

The "default" option will still not show up in the help:

python test_parser.py -h
usage: test_parser.py [-h] {hi,hai} ...

positional arguments:
  {hi,hai}

optional arguments:
  -h, --help  show this help message and exit

However, it is now possible to differentiate between and separately handle calling one of the provided subparsers, and calling the default subparser when no argument was provided:

$ python test_parser.py hi
Namespace(cmd='hi')
$ python test_parser.py 
Namespace(cmd='hey')
Sully answered 15/10, 2014 at 10:0 Comment(7)
This is a great solution (despite being old) - best one I found with extensive googling. However, it only seems to support the case where the default_subparser is one of previously defined subparsers. I made a small modification that would allow for statements such as a.set_default_subparser('no_sp') which could then be handled separately, and in a way that it does not pollute the help output. However, it only adds 9 lines of code so I don't think it deserves a separate answer - would you mind if I updated your answer with these modifications of mine?Salpinx
@Salpinx Argparse hasn't changed much, that makes old answer still have value. Yes, you can update the answer, I might even include the changes in ruamel.std.argparse ;-) Please add your "version" as a separate block at the bottom of answer introduced by something like: --- If you want to select the default subparser by the string representation of its name...Sully
There, I added my version with comments in the code and examples to demonstrate functionality. There is one place in the code - my loop starting with for x in self._subparsers._actions: - where I am not sure if I need to do additional checks, which you might be more familiar with. This did make the answer a little bit massive - so maybe take a look at the code differences and decide yourself if you prefer to keep it as two versions like now or merge it after all. Hope this helps someone :)Salpinx
Actually, I found a slightly undesirable behavior: when a non-existant option is specified (e.g. python test_parser.py ho) the usage description will also list the default subparser value: usage: test_parser.py [-h] {hi,hai,hey} ... (the default subparser was added since no matching one was found). It is not a big issue, but if you know how to rectify it it would be cool :)Salpinx
I might have found a bug in your original posted code. If the parser also contains a required optional argument (e.g. usage: test_parser.py [-h] -i IMAGE {hi,hai} ...), the provided -i argument is ignored when the "default subparser" is used, e.g. python test_parser.py -i original1.png produces the following error: test_parser.py: error: argument -i/--image is required. Solution: changing sys.argv.insert(0,name) into sys.argv.insert(len(sys.argv), name) and same for args case. When inserting to position 0, all arguments are ignored due to entering the subparser immediately.Salpinx
@Salpinx This requires more than cursory investigation, I hope I have some time for that this weekend.Sully
I worked around the bug by adding a new optional argument that can be used to pass the number of global positional arguments to set_default_subparser, then perform the insertion into the argument array as: sys.argv.insert(sys.argv.insert(len(sys.argv) - number_of_positional_args, name) and args.insert(len(args) - number_of_positional_args, name). This inserts default in the last position before global positional arguments, but implies no global options are specified after first positional argument, and that the number of global positional arguments is always the same.Ninetta
A
8

It seems I've stumbled on the solution eventually myself.

If the command is optional, then this makes the command an option. In my original parser configuration, I had a package command that could take a range of possible steps, or it would perform all steps if none was given. This makes the step a choice:

parser = argparse.ArgumentParser()

command_parser = subparsers.add_parser('command')
command_parser.add_argument('--step', choices=['prepare', 'configure', 'compile', 'stage', 'package'])

...other command parsers

parsed_args = parser.parse_args()

if parsed_args.step is None:
    do all the steps...
Atlantean answered 18/6, 2011 at 9:9 Comment(5)
However this means if different subcommands have different flags you have to handle that logic yourself rather than letting argparse take care of it.Prefabricate
Don't understand what you mean by this, the code doesn't look any different. Did you remove "required" somewhere?Comport
What I mean is that because the command "step", is optional above, it is better treated as a flag.Atlantean
A more elegant way would be to use the dest value. No need to rely on the arguments. docsMarleen
"If the command is optional, then this makes the command an option." This is not always true. For example, git remote by default acts as git remote show, but show is not an option, but a subcommand (like add, rm, etc). However, to simulate that I manually checked the arguments, as you propose.Howler
H
6

Here's a nicer way of adding a set_default_subparser method:

class DefaultSubcommandArgParse(argparse.ArgumentParser):
    __default_subparser = None

    def set_default_subparser(self, name):
        self.__default_subparser = name

    def _parse_known_args(self, arg_strings, *args, **kwargs):
        in_args = set(arg_strings)
        d_sp = self.__default_subparser
        if d_sp is not None and not {'-h', '--help'}.intersection(in_args):
            for x in self._subparsers._actions:
                subparser_found = (
                    isinstance(x, argparse._SubParsersAction) and
                    in_args.intersection(x._name_parser_map.keys())
                )
                if subparser_found:
                    break
            else:
                # insert default in first position, this implies no
                # global options without a sub_parsers specified
                arg_strings = [d_sp] + arg_strings
        return super(DefaultSubcommandArgParse, self)._parse_known_args(
            arg_strings, *args, **kwargs
        )
Hiroshima answered 2/6, 2016 at 13:42 Comment(2)
Thanks for the code, but it's usually better to describe in text the idea of what your code does.Howler
the idea is to not monkeypatch argparse.ArgumentParser and instead subclass itHiroshima
P
4

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.

Pebbly answered 19/5, 2019 at 9:11 Comment(1)
This should work for the original question, but unfortunately this doesn't work when you set a default action for a subparser (it gives "AttributeError: '_SubParsersAction' object has no attribute 'set_defaults'"). I'm trying to simulate "git remote" - by default it just prints the remotes, but it can have subcommands like "add", "rm", etc.Howler
L
3

Maybe what you're looking for is the dest argument of add_subparsers:

(Warning: works in Python 3.4, but not in 2.7)

import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='cmd')
parser_hi = subparsers.add_parser('hi')
parser.parse_args([]) # Namespace(cmd=None)

Now you can just use the value of cmd:

if cmd in [None, 'hi']:
    print('command "hi"')
Lengel answered 30/1, 2016 at 10:36 Comment(2)
Doesn't this approach (as written) bypass any values in sys.argv in favor of the supplied empty list ([])?Empyema
@Empyema of course in a real usage scenario do not pass [] to parse_args, here I am doing it to show what arguments we are passing (none) for the example.Hengelo
E
2

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')
Empyema answered 30/5, 2019 at 20:9 Comment(0)
Y
1

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'
Yerga answered 30/6, 2022 at 19:48 Comment(0)
G
0

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
Geophilous answered 29/8, 2018 at 15:48 Comment(0)
A
-1

You can add an argument with a default value that will be used when nothing is set I believe.

See this: http://docs.python.org/dev/library/argparse.html#default

Edit:

Sorry, I read your question a bit fast.

I do not think you would have a direct way of doing what you want via argparse. But you could check the length of sys.argv and if its length is 1 (only script name) then you could manually pass the default parameters for parsing, doing something like this:

import argparse

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

if len(sys.argv) == 1:
   a.parse_args(['hi'])
else:
   a.parse_args()

I think that should do what you want, but I agree it would be nice to have this out of the box.

Aldus answered 15/6, 2011 at 23:27 Comment(1)
This is a bad solution; what if there are additional flags?Prefabricate
L
-1

For later reference:

...
b = a.add_subparsers(dest='cmd')
b.set_defaults(cmd='hey')  # <-- this makes hey as default

b.add_parser('hi')

so, these two will be same:

  • python main.py hey
  • python main.py
Languishment answered 11/4, 2018 at 12:8 Comment(1)
There is no set_defaults in the result of add_subparsersOgive

© 2022 - 2024 — McMap. All rights reserved.