How can you allow for top-level program arguments to be added after using a subcommand from a subparser?
I have a program that includes several subparsers to allow for subcommands, changing the behavior of the program. Here is an example of how its set up:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
def task_a():
print('did task_a')
def task_c():
print('did task_c')
def task_d():
print('did task_d')
def run_foo(args):
a_arg = args.a
c_arg = args.c
if a_arg:
task_a()
if c_arg:
task_c()
def run_bar(args):
a_arg = args.a
d_arg = args.d
if a_arg:
task_a()
if d_arg:
task_d()
def parse():
'''
Run the program
arg parsing goes here, if program was run as a script
'''
# create the top-level parser
parser = argparse.ArgumentParser()
# add top-level args
parser.add_argument("-a", default = False, action = "store_true", dest = 'a')
# add subparsers
subparsers = parser.add_subparsers(title='subcommands', description='valid subcommands', help='additional help', dest='subparsers')
# create the parser for the "foo" command
parser_foo = subparsers.add_parser('foo')
parser_foo.set_defaults(func = run_foo)
parser_foo.add_argument("-c", default = False, action = "store_true", dest = 'c')
# create the parser for the "bar" downstream command
parser_bar = subparsers.add_parser('bar')
parser_bar.set_defaults(func = run_bar)
parser_bar.add_argument("-d", default = False, action = "store_true", dest = 'd')
# parse the args and run the default parser function
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
parse()
When I run the program I can call a subcommand with its args like this:
$ ./subparser_order.py bar -d
did task_d
$ ./subparser_order.py foo -c
did task_c
But if I want to include the args from the top level, I have to call it like this:
$ ./subparser_order.py -a foo -c
did task_a
did task_c
However, I think this is confusing, especially if there are many top-level args and many subcommand args; the subcommand foo
is sandwiched in the middle here and harder to discern.
I would rather be able to call the program like subparser_order.py foo -c -a
, but this does not work:
$ ./subparser_order.py foo -c -a
usage: subparser_order.py [-h] [-a] {foo,bar} ...
subparser_order.py: error: unrecognized arguments: -a
In fact, you cannot call the top-level args at all after specifying a subcommand:
$ ./subparser_order.py foo -a
usage: subparser_order.py [-h] [-a] {foo,bar} ...
subparser_order.py: error: unrecognized arguments: -a
Is there a solution that will allow for the top-level args to be included after the subcommand?
Subparsers are invoked based on the value of the first positional argument, so your call would look like python test01.py A a1 -v 61. The "A" triggers the appropriate subparser, which would be defined to allow a positional argument and the -v option.
To add an optional argument, simply omit the required parameter in add_argument() . args = parser. parse_args()if args.
Python argparse The argparse module makes it easy to write user-friendly command-line interfaces. It parses the defined arguments from the sys. argv . The argparse module also automatically generates help and usage messages, and issues errors when users give the program invalid arguments.
There is actually a way to do it. You can use parse_known_args
, take the namespace and unparsed arguments and pass these back to a parse_args
call. It will combine and override in the 2nd pass and any left over arguments from there on will still throw parser errors.
Simple example, here is the setup:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-a', action='store_true')
sp = parser.add_subparsers(dest='subargs')
sp_1 = sp.add_parser('foo')
sp_1.add_argument('-b', action='store_true')
print(parser.parse_args())
In the proper order for argparse to work:
- $ python3 argparse_multipass.py
Namespace(a=False, subargs=None)
- $ python3 argparse_multipass.py -a
Namespace(a=True, subargs=None)
- $ python3 argparse_multipass.py -a foo
Namespace(a=True, b=False, subargs='foo')
- $ python3 argparse_multipass.py foo
Namespace(a=False, b=False, subargs='foo')
- $ python3 argparse_multipass.py foo -b
Namespace(a=False, b=True, subargs='foo')
- $ python3 argparse_multipass.py -a foo -b
Namespace(a=True, b=True, subargs='foo')
Now, you can't parse arguments after a subparser kicks in:
- $ python3 argparse_multipass.py foo -b -a
usage: argparse_multipass.py [-h] [-a] {foo} ...
argparse_multipass.py: error: unrecognized arguments: -a
However, you can do a multi-pass to get your arguments back. Here is the updated code:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-a', action='store_true')
sp = parser.add_subparsers(dest='subargs')
sp_1 = sp.add_parser('foo')
sp_1.add_argument('-b', action='store_true')
args = parser.parse_known_args()
print('Pass 1: ', args)
args = parser.parse_args(args[1], args[0])
print('Pass 2: ', args)
And the results for it:
- $ python3 argparse_multipass.py
Pass 1: (Namespace(a=False, subargs=None), [])
Pass 2: Namespace(a=False, subargs=None)
- $ python3 argparse_multipass.py -a
Pass 1: (Namespace(a=True, subargs=None), [])
Pass 2: Namespace(a=True, subargs=None)
- $ python3 argparse_multipass.py -a foo
Pass 1: (Namespace(a=True, b=False, subargs='foo'), [])
Pass 2: Namespace(a=True, b=False, subargs='foo')
- $ python3 argparse_multipass.py foo
Pass 1: (Namespace(a=False, b=False, subargs='foo'), [])
Pass 2: Namespace(a=False, b=False, subargs='foo')
- $ python3 argparse_multipass.py foo -b
Pass 1: (Namespace(a=False, b=True, subargs='foo'), [])
Pass 2: Namespace(a=False, b=True, subargs='foo')
- $ python3 argparse_multipass.py -a foo -b
Pass 1: (Namespace(a=True, b=True, subargs='foo'), [])
Pass 2: Namespace(a=True, b=True, subargs='foo')
- $ python3 argparse_multipass.py foo -b -a
Pass 1: (Namespace(a=False, b=True, subargs='foo'), ['-a'])
Pass 2: Namespace(a=True, b=True, subargs='foo')
This will maintain original functionality but allow continued parsing for when subparsers kick in. Additionally you could make disordered parsing out of the thing entirely if you do something like this:
ns, ua = parser.parse_known_args()
while len(ua):
ns, ua = parser.parse_known_args(ua, ns)
It will keep parsing arguments in case they are out of order until it has completed parsing all of them. Keep in mind this one will keep going if there is an unknown argument that stays in there. Mind want to add something like this:
pua = None
ns, ua = parser.parse_known_args()
while len(ua) and ua != pua:
ns, ua = parser.parse_known_args(ua, ns)
pua = ua
ns, ua = parser.parse_args(ua, ns)
Just keep a previously unparsed arguments object and compare it, when it breaks do a final parse_args
call to make the parser run its own errors path.
It's not the most elegant solution but I ran into the exact same problem where my arguments on the main parser were used as optional flags additionally on top of what was specified in a sub parser.
Keep the following in mind though: This code will make it so a person can specify multiple subparsers and their options in a run, the code that these arguments invoke should be able to deal with that.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With