How to parse multiple nested sub-commands using python argparse?

I am implementing a command line program which has interface like this:

cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]

I have gone through the argparse documentation. I can implement GLOBAL_OPTIONS as optional argument using add_argument in argparse. And the {command [COMMAND_OPTS]} using Sub-commands.

From the documentation it seems I can have only one sub-command. But as you can see I have to implement one or more sub-commands. What is the best way to parse such command line arguments useing argparse?


I came up with the same qustion, and it seems i have got a better answer.

The solution is we shall not simply nest subparser with another subparser, but we can add subparser following with a parser following another subparser.

Code tell you how:

parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('--user', '-u',
                    default=getpass.getuser(),
                    help='username')
parent_parser.add_argument('--debug', default=False, required=False,
                        action='store_true', dest="debug", help='debug flag')
main_parser = argparse.ArgumentParser()
service_subparsers = main_parser.add_subparsers(title="service",
                    dest="service_command")
service_parser = service_subparsers.add_parser("first", help="first",
                    parents=[parent_parser])
action_subparser = service_parser.add_subparsers(title="action",
                    dest="action_command")
action_parser = action_subparser.add_parser("second", help="second",
                    parents=[parent_parser])

args = main_parser.parse_args()

@mgilson has a nice answer to this question. But problem with splitting sys.argv myself is that i lose all the nice help message Argparse generates for the user. So i ended up doing this:

import argparse

## This function takes the 'extra' attribute from global namespace and re-parses it to create separate namespaces for all other chained commands.
def parse_extra (parser, namespace):
  namespaces = []
  extra = namespace.extra
  while extra:
    n = parser.parse_args(extra)
    extra = n.extra
    namespaces.append(n)

  return namespaces

argparser=argparse.ArgumentParser()
subparsers = argparser.add_subparsers(help='sub-command help', dest='subparser_name')

parser_a = subparsers.add_parser('command_a', help = "command_a help")
## Setup options for parser_a

## Add nargs="*" for zero or more other commands
argparser.add_argument('extra', nargs = "*", help = 'Other commands')

## Do similar stuff for other sub-parsers

Now after first parse all chained commands are stored in extra. I reparse it while it is not empty to get all the chained commands and create separate namespaces for them. And i get nicer usage string that argparse generates.


parse_known_args returns a Namespace and a list of unknown strings. This is similar to the extra in the checked answer.

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--foo')
sub = parser.add_subparsers()
for i in range(1,4):
    sp = sub.add_parser('cmd%i'%i)
    sp.add_argument('--foo%i'%i) # optionals have to be distinct

rest = '--foo 0 cmd2 --foo2 2 cmd3 --foo3 3 cmd1 --foo1 1'.split() # or sys.argv
args = argparse.Namespace()
while rest:
    args,rest =  parser.parse_known_args(rest,namespace=args)
    print args, rest

produces:

Namespace(foo='0', foo2='2') ['cmd3', '--foo3', '3', 'cmd1', '--foo1', '1']
Namespace(foo='0', foo2='2', foo3='3') ['cmd1', '--foo1', '1']
Namespace(foo='0', foo1='1', foo2='2', foo3='3') []

An alternative loop would give each subparser its own namespace. This allows overlap in positionals names.

argslist = []
while rest:
    args,rest =  parser.parse_known_args(rest)
    argslist.append(args)