2

With argparse I would like to be able to mix optional parameters with multiple positional parameters, e.g., like svn allows:

svn ls first/path -r 1000 second/path

At the moment, this is not officially supported by Python (c.f. http://bugs.python.org/issue14191). I wrote this workaround and I am now wondering, if a) there is a better/easier/more elegant way to do it, and b) if someone can see something in the code that might break it under certain cirumstances:

#!/usr/bin/env python3                                                          

import argparse as ap                                                           

p = ap.ArgumentParser()                                                         
p.add_argument('-v', action='store_true')                                       
p.add_argument('-l', action='store_true')                                       
p.add_argument('files', nargs='*', action='append')                             
p.add_argument('remainder', nargs=ap.REMAINDER, help=ap.SUPPRESS)                  

args = p.parse_args()                                                              
while args.remainder != []:                                                        
    args = p.parse_args(args.remainder, args)                                      

print(args)  

Usage example:

./test.py a b -v c d 

Output:

Namespace(files=[['a', 'b'], ['c', 'd']], l=False, remainder=[], v=True)
  • There's a version of the workaround in 14191 that you can download and add to your code - without modifying your `argparse`. You may need it if you need to define more `positionals`. – hpaulj Nov 03 '14 at 16:37

2 Answers2

2

You could use parse_known_args instead of including a remainder:

import argparse as ap                                                           

p = ap.ArgumentParser()                                                         
p.add_argument('-v', action='store_true')                                       
p.add_argument('-l', action='store_true')                                       
p.add_argument('files', nargs='*', action='append')                             

args, unknown = p.parse_known_args()
while unknown:
    args, unknown = p.parse_known_args(unknown, args) 

print(args)

yields

Namespace(files=[['a', 'b'], ['c', 'd']], l=False, v=True)
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
0

Do you want files=[['a', 'b'], ['c', 'd']] or files=['a', 'b', 'c', 'd']? In other words should

./test.py a b -v c d 
./test.py a b -v c -l d 
./test.py -l a b -v c d 

give different files lists of lists.

append with a * positional usually doesn't make sense, since you can't repeat a positional argument. But with this recursive application, it does work. But if the sublists are important, why not use multiple positional arguments.

On the other hand, to get a flat list of 'files', you could do several things:

You could flatten the list after parsing (e.g. args.files=list(itertools.chain(*args.files)))

You could use p.add_argument('files', nargs='?', action='append'). This iterates over each file string.

./test.py a b -l c d -v e f
Namespace(files=['a', 'b', 'c', 'd', 'e', 'f'], l=True, remainder=[], v=True)

You could replicate the http://bugs.python.org/issue14191 patch by removing the positional from the initial parse. In this case the extras can be simply inserted into args.

A disadvantage of this is that the usage and help known nothing about the positional, requiring a custom usage and/or description parameter.

usage = usage: %(prog)s [-h] [-v] [-l] [files [files ...]]
description = 'files: may be given in any order'
p = ap.ArgumentParser(usage=usage, description=description)                            
p.add_argument('-v', action='store_true')                                       
p.add_argument('-l', action='store_true')                                       
args, extras = p.parse_known_args() 
args.files = extras

unutbu's answer does not preserve the groupings.  They are lost the first time through:

    Namespace(files=[['a', 'b'], ['c', 'd', 'e', 'f']], l=True, v=True)

It could be changed to give a flat list:

p = ap.ArgumentParser()
p.add_argument('-v', action='store_true')
p.add_argument('-l', action='store_true')
p.add_argument('files', nargs='*')

args, unknown = p.parse_known_args()
args.files.extend(unknown)

The iteration isn't needed, since optionals are handled the first time through. All that is left in unknown are files.

In sum - to preserve the groupings, your solution appears to be the best.

hpaulj
  • 221,503
  • 14
  • 230
  • 353