2

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?

Kirill Zaitsev
  • 4,511
  • 3
  • 21
  • 29
  • Have you tried looking at argparse's [sub-commands](https://docs.python.org/2/library/argparse.html#sub-commands)? Looks like it may work, but I'm not sure if you could put multiple sub-commands on a single parser line. – Sandy Chapman Apr 10 '15 at 13:02
  • I've been playing with sub-parsers and groups for some time now, but with no success. It seems, that both of the options have goals different from mine. I wonder If I'll be able to bend any of them to serve my needs. – Kirill Zaitsev Apr 10 '15 at 13:08

1 Answers1

1

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.

Community
  • 1
  • 1
hpaulj
  • 221,503
  • 14
  • 230
  • 353