Setting up a truly non-required subparser with argparse

50 Views Asked by At

I'd like my program to allow any string to be passed as an argument, but also to allow subcommands. For example, these would all be valid:

$ python argtest.py 'hello world'
$ python argtest.py version
$ python argtest.py help

I do not want users to have to use --, e.g., python argparse.py -- 'hello world'.

If I set up a subcommand parser to handle 'version' and 'help', it will then complain that 'hello world' isn't a valid subcommand.

My code:

parser = argparse_flags.ArgumentParser()
subparsers = parser.add_subparsers(required=False)
subparsers.add_parser('version')
subparsers.add_parser('help')
ns, remainder = parser.parse_known_args(argv[1:])

Yields:

$ python argtest.py 'hello world'
usage: argtest.py [-h] {version,help} ...
argtest.py: error: invalid choice: 'hello world' (choose from 'version', 'help')

The only fix I've come up with is to create a "fallback" subcommand and then manually add it with parser.parse_known_args(['fallback'] + argv[1:]) by either:

  • Looking for this first argument not prefixed with '-' and then see if it's in subparsers.choices.
  • Catch the SystemExit raised by argparse when no subparsers match (and temporarily redirecting stderr so it doesn't spam the CLI).

All of this is kind of gross, is there a better way to get the behavior I want? I just want argparse to be a little more chill about not finding a subcommand.

1

There are 1 best solutions below

0
hpaulj On BEST ANSWER

add_subparsers is a variant on add_argument, creating a Action subclass with nargs=argparse.PARSER.

Only optionals get their flag string selected by value. Positionals are allocated strictly by postion and nargs. This is done in _get_values. For a parser

 # PARSER arguments convert all values, but check only the first
        elif action.nargs == PARSER:
            value = [self._get_value(action, v) for v in arg_strings]
            self._check_value(action, value[0])

So all remaining strings in arg_strings are allocated. _get_value tests against type, which here is just a pass through str. _check_value tests the first of that list against choices.

For subparsers, the choices are the subparser names and their aliases. So this is where your 'hello_world' fails.

As you note, exit errors can be caught. Recently a exit_on_error parameter has been added that will raise the error instead of exiting. It doesn't work in all cases, but I think it should work here. And as documented you can also tweak the error and exit methods to change how errors are raised.

Normally a positional is not-required only if nargs is '?' or '*'. And a due to tweak of old patching, also this 'PARSER' case. There's no mechanism to retry allocating a string if it fails the type or choices.

If there are multiple positionals, strings are allocated in a re greedy sense. First argument gets as many values as it wants, etc. So be careful when stringing '+*?' nargs together.

In sum, I think the subparsers mechanism works best if you define a bunch of optionals for the main parsers, and then the add_subparsers Action. That way it can handle the optionals, if any, and then branch to the chosen subparser to handle the rest. A positional can be defined for the main parser, but avoid open ended nargs.

Another thing with subparsers. Use different argument dest in the main and subparsers. That way the subparser defaults won't step on main parser defaults.