Kirill Zaitsev
Kirill Zaitsev

Reputation: 4849

Argparse: Optional arguments, distinct for different positional arguments

I want to have positional arguments with an optional argument. Smth like my_command foo --version=0.1 baz bar --version=0.2. That should parse into a list of args [foo, bar, baz] with version set for 2 of them.

Without optional argument it's trivial to set nargs=* or nargs=+, but I'm struggling with providing an optional argument for positional ones. Is it even possible with argparse?

Upvotes: 2

Views: 781

Answers (1)

hpaulj
hpaulj

Reputation: 231355

Multiple invocation of the same subcommand in a single command line

This tries to parse something like

$ python test.py executeBuild --name foobar1 executeBuild --name foobar2 ....

Both proposed solutions call a parser multiple times. Each call handles a cmd --name value pair. One splits sys.argv before hand, the other collects unparsed strings with a argparse.REMAINDER argument.

Normally optionals can occur in any order. They are identified solely by that - flag value. Positionals have to occur in a particular order, but optionals may occur BETWEEN different positionals. Note also that in the usage display, optionals are listed first, followed by positionals.

 usage: PROG [-h] [--version Version] [--other OTHER] FOO BAR BAZ

subparsers are the only way to link a positional argument with one or more optionals. But normally a parser is allowed to have only one subparsers argument.

Without subparsers, append is the only way to collect data from repeated uses of an optional:

parser.add_argument('--version',action='append')
parser.add_argument('foo')
parser.add_argument('bar')
parser.add_argument('baz')

would handle your input string, producing a namespace like:

namespace(version=['0.1','0.2'],foo='foo',bar='bar',baz='baz')

But there's no way of identifying baz as the one that is 'missing' a version value.

Regarding groups - argument groups just affect the help display. They have nothing to do with parsing.

How would you explain to your users (or yourself 6 mths from now) how to use this interface? What would the usage and help look like? It might be simpler to change the design to something that is easier to implement and to explain.


Here's a script which handles your sample input.

import argparse
usage = 'PROG [cmd [--version VERSION]]*'
parser = argparse.ArgumentParser(usage=usage)
parser.add_argument('cmd')
parser.add_argument('-v','--version')
parser.add_argument('rest', nargs=argparse.PARSER)
parser.print_usage()
myargv = 'foo --version=0.1 baz bar --version=0.2'.split()
# myargv = sys.argv[1:] # in production

myargv += ['quit']  # end loop flag
args = argparse.Namespace(rest=myargv)
collect = argparse.Namespace(cmd=[])
while True:
    args = parser.parse_args(args.rest)
    collect.cmd += [(args.cmd, args.version)]
    print(args)
    if args.rest[0]=='quit':
        break
print collect

It repeatedly parses a positional and optional, collecting the rest in a argparse.PARSER argument. This is like + in that it requires at least one string, but it collects ones that look like optionals as well. I needed to add a quit string so it wouldn't throw an error when there wasn't anything to fill this PARSER argument.

producing:

usage: PROG [cmd [--version VERSION]]*
Namespace(cmd='foo', rest=['baz', 'bar', '--version=0.2', 'quit'], version='0.1')
Namespace(cmd='baz', rest=['bar', '--version=0.2', 'quit'], version=None)
Namespace(cmd='bar', rest=['quit'], version='0.2')
Namespace(cmd=[('foo', '0.1'), ('baz', None), ('bar', '0.2')])

The positional argument that handles subparsers also uses this nargs value. That's how it recognizes and collects a cmd string plus everything else.

So it is possible to parse an argument string such as you want. But I'm not sure the code complexity is worth it. The code is probably fragile as well, tailored to this particular set of arguments, and just a few variants.

Upvotes: 1

Related Questions