2

Is it possible to create a set of optional arguments that are related and repeatable?

Let's say I have three arguments -a, -b, -c that form a set, -a is required, but -b and -c are optional. (Updated)

I would like an ability to specify multiple set of these.

Script.py -a 1 -b 2 -c 3    -a 4 -c 6   -a 7 -b 8    -a 10

This will be parsed as a list of dict as follows

[
    {"a":1, "b":2, "c":3},
    {"a":4, "c":6},
    {"a":7, "b":8},
    {"a":10}
]
user3043805
  • 245
  • 2
  • 10
  • How are you going to specify set boundaries on the command line? using multiple spaces is a visual help, but when your shell parses the command for execution, multiple spaces are not going to help. So your Python app will just receive: `-a 1 -b 2 -c 3 -a 4 -c 6 -b 8 -a 10`. How do you define sets now? Is it a valid assumption that elements of each set are sorted, so you'd know you've reached another set because you had _c_ before, and now you've reached to _a_? – farzad Oct 09 '14 at 03:44
  • The spaces were just to distinctly show possible interaction. I agree there is no good way to break them into sets without a required argument, thanks for pointing this out, I should have mentioned that -a is a required argument, this lets us to identify set boundaries. I thought to simply use list for each argument and zip them as tuples, but that will be incorrect when some optional args are missing. – user3043805 Oct 09 '14 at 03:57

3 Answers3

2

A way to make arguments repeatable is to use an 'append' action type:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-a', action='append')
parser.add_argument('-b', action='append')
parser.add_argument('-c', action='append')
argv = '-a 1 -b 2 -c 3    -a 4 -c 6   -a 7 -b 8    -a 10'
args = parser.parse_args(argv.split())
print args

produces:

Namespace(a=['1', '4', '7', '10'], b=['2', '8'], c=['3', '6'])

Unfortunately it does lose some information. There's no way to associate the '4' with the '6' instead of the '8'.

If you use '--' to separate blocks of arguments, then this iterative parser might do the job:

parser = argparse.ArgumentParser()
# SUPPRESS keeps the default None out of the namespace
parser.add_argument('-a', type=int, default=argparse.SUPPRESS, required=True)
parser.add_argument('-b', type=int, default=argparse.SUPPRESS)
parser.add_argument('-c', type=int, default=argparse.SUPPRESS)
argv = '-a 1 -b 2 -c 3  --  -a 4 -c 6 --  -a 7 -b 8  --  -a 10'

arglist = []
rest = argv.split()
while rest:
    args,rest = parser.parse_known_args(rest)
    rest = rest[1:]  # remove the 1st '--'
    print args
    arglist.append(vars(args))
print arglist

producing:

Namespace(a=1, b=2, c=3)
Namespace(a=4, c=6)
Namespace(a=7, b=8)
Namespace(a=10)

[{'a': 1, 'c': 3, 'b': 2}, 
 {'a': 4, 'c': 6}, 
 {'a': 7, 'b': 8}, 
 {'a': 10}]

I'm not sure if it is robust enough. I made -a required, so omitting it from one of the groups will raise an error.


Or adapting farzad's iterator:

def by_sets(iterator, start):
    set = []
    for val in iterator:
        if set and val == start:
            yield set
            set = [val]
        else:
            set.append(val)
    yield set

argv = '-a 1 -b 2 -c 3  -a 4 -c 6 -a 7 -b 8  -a 10'
# print list(by_sets(argv.split(), '-a'))
# [['-a', '1', '-b', '2', '-c', '3'], ['-a', '4', '-c', '6'],... ['-a', '10']]

arglist = []
for aset in by_sets(argv.split(), '-a'):
    arglist.append(vars(parser.parse_args(aset)))
print arglist

produces:

[{'a': 1, 'c': 3, 'b': 2}, {'a': 4, 'c': 6}, {'a': 7, 'b': 8}, {'a': 10}]

The loop can also be written as a comprehension:

[vars(parser.parse_args(aset)) for aset in by_sets(argv.split(), '-a')]
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • Great, thanks, accepting hpaulj's solution as the question was argparse specific., though farzad provided helpful comments and code. Thank you both – user3043805 Oct 09 '14 at 21:09
1

We could iterate over command line args and add them one by one to the set, if we reached to '-a' which marks the start of another set, we'll create a new set. This code sample does not check for invalid user input though.

import sys

def get_pairs(iterator, start):
    sets = []
    for val in iterator:
        if val == start:
            sets.append({})
        sets[-1][val] = next(iterator)
    return sets

print get_pairs(iter(sys.argv[1:]), '-a')
farzad
  • 8,775
  • 6
  • 32
  • 41
0

Thank you, @hpaulj and @farzad – I found your answers very helpful. Another way to implement farzad's iterator:

more_itertools.split_before(arg_list, lambda arg: arg == '-a')
Shi
  • 91
  • 4