How to mix shared and exclusive arguments in argparse

Viewed 130

I want to implement commands with argparse and share common arguments between them. At the same time these commands have some unique arguments that should only be available to the specific parser.

My current code looks like this:

parser = argparse.ArgumentParser(description='test')
parser.add_argument('command', help='Subcommand to run')

parser.add_argument("--opta",
                    help="usa a",
                    action="store_true")
parser.add_argument("--optb",
                    help="Use b",
                    action="store_true")

subparser = parser.add_subparsers(help="")

parser_1 = subparser.add_parser("cmd1",
                                help="do 1",
                                parents=[parser], add_help=False)

parser_1.add_argument("--yes", "-y",
                      help="just do it",
                      action="store_true")
parser_1.add_argument("--dry-run", "-s",
                      help="Simulate actions",
                      action="store_true")

parser_2 = subparser.add_parser("cmd2",
                                help="do 2",
                                parents=[parser], add_help=False)

parser_2.add_argument('--choice', choices=['c', ''], default='',
                      help="choose something")

print(sys.argv)
args = parser.parse_known_args()
print(args)

When executing this code I get an error message by argparse claiming the input to be unknown.

./test.py cmd2 --opta --choice c
['./test.py', 'cmd2', '--opta', '--choice', 'c']
usage: test.py [-h] [--opta] [--optb] command {cmd1,cmd2} ...
test.py: error: invalid choice: 'c' (choose from 'cmd1', 'cmd2')

cmd1 however seems to work.

./test.py cmd1 --opta --yes
['./test.py', 'cmd1', '--opta', '--yes']
(Namespace(command='cmd1', opta=True, optb=False), ['--yes'])

What am I overlooking? Is there a more elegant way of using argparse for this?

1 Answers

Your problem comes from the parser.add_argument('command', help='Subcommand to run') line which makes the main parser intercept the command and prevents it to be parsed by any subparser.

Simply removing this line makes it work: The cmd1or cmd2 positional argument is no more intercepted and it is free to be used by subprasers.

$ python ./test.py cmd1 --opta
['./test.py', 'cmd1', '--opta']
(Namespace(dry_run=False, opta=True, optb=False, yes=False), [])
$ python ./test.py cmd2 --opta --choice c
['./test.py', 'cmd2', '--opta', '--choice', 'c']
(Namespace(choice='c', opta=True, optb=False), [])

This is almost fine. "Almost" because you probably want to get the actual command name to be stored in the arguments (which is not the case currently).

You could provide a dest parameter to the add_subparsers method and remove the parents=[parser] of the subparsers.add_parser calls:

subparser = parser.add_subparsers(dest='command')
...
parser_1 = subparser.add_parser("cmd1", help="do 1", add_help=False)
...
parser_2 = subparser.add_parser("cmd2", help="do 2", add_help=False)

But then the problem is that --opta and --optb are now part of the main parser and have to be provided before the command:

$ python ./test.py cmd1 --opta
['./test.py', 'cmd1', '--opta']
(Namespace(dry_run=False, opta=False, optb=False, yes=False), ['--opta'])
$ python ./test.py --opta cmd1
['./test.py', '--opta', 'cmd1']
(Namespace(dry_run=False, opta=True, optb=False, yes=False), [])

A way to deal with that would be to make the --opta and --optb part of the subparsers without repeating their declaration. It can be done by using a custom ArgumentParser subclass which declares the common options:

class SubCommandParser(argparse.ArgumentParser):
    def __init__(self, *args, **kwargs):
        super(SubCommandParser, self).__init__(*args, **kwargs)
        self.add_argument("--opta", help="usa a", action="store_true")
        self.add_argument("--optb", help="use b", action="store_true")

Then you can inform the add_subparsers method that this SubCommandParser class should be used for subparsers instantiation:

subparsers = self.add_subparsers(parser_class=SubCommandParser)

Doing it, each subcommand will be an instance of SubCommandParser and have the --opta and --optb options.

$ python ./test.py cmd1 --opta
(Namespace(command='cmd1', dry_run=False, opta=True, optb=False, yes=False), [])
$ python ./test.py cmd2 --opta --choice c
(Namespace(choice='c', command='cmd2', opta=True, optb=False), [])
Related