140

I'm trying to write a script that accepts multiple input sources and does something to each one. Something like this

./my_script.py \
    -i input1_url input1_name input1_other_var \
    -i input2_url input2_name input2_other_var \
    -i input3_url input3_name
# notice inputX_other_var is optional

But I can't quite figure out how to do this using argparse. It seems that it's set up so that each option flag can only be used once. I know how to associate multiple arguments with a single option (nargs='*' or nargs='+'), but that still won't let me use the -i flag multiple times. How do I go about accomplishing this?

Just to be clear, what I would like in the end is a list of lists of strings. So

[["input1_url", "input1_name", "input1_other"],
 ["input2_url", "input2_name", "input2_other"],
 ["input3_url", "input3_name"]]
Michael
  • 8,362
  • 6
  • 61
  • 88
John Allard
  • 3,564
  • 5
  • 23
  • 42
  • 1
    So why not associate the multiple input source arguments with that single option? – TigerhawkT3 Mar 22 '16 at 22:16
  • Because each of the multiple input sources also need to have multiple string arguments. I'd like to have to use the -i flag for each one of the inputs, and each input would contain all the strings between successive -i flags. I want it to work like ffmpeg where you specify inputs with -i – John Allard Mar 22 '16 at 22:17

4 Answers4

129

Here's a parser that handles a repeated 2 argument optional - with names defined in the metavar:

parser=argparse.ArgumentParser()
parser.add_argument('-i','--input',action='append',nargs=2,
    metavar=('url','name'),help='help:')

In [295]: parser.print_help()
usage: ipython2.7 [-h] [-i url name]

optional arguments:
  -h, --help            show this help message and exit
  -i url name, --input url name
                        help:

In [296]: parser.parse_args('-i one two -i three four'.split())
Out[296]: Namespace(input=[['one', 'two'], ['three', 'four']])

This does not handle the 2 or 3 argument case (though I wrote a patch some time ago for a Python bug/issue that would handle such a range).

How about a separate argument definition with nargs=3 and metavar=('url','name','other')?

The tuple metavar can also be used with nargs='+' and nargs='*'; the 2 strings are used as [-u A [B ...]] or [-u [A [B ...]]].

hpaulj
  • 221,503
  • 14
  • 230
  • 353
110

This is simple; just add both action='append' and nargs='*' (or '+').

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-i', action='append', nargs='+')
args = parser.parse_args()

Then when you run it, you get

In [32]: run test.py -i input1_url input1_name input1_other_var -i input2_url i
...: nput2_name input2_other_var -i input3_url input3_name

In [33]: args.i
Out[33]:
[['input1_url', 'input1_name', 'input1_other_var'],
 ['input2_url', 'input2_name', 'input2_other_var'],
 ['input3_url', 'input3_name']]
Michael
  • 8,362
  • 6
  • 61
  • 88
Amir
  • 1,871
  • 1
  • 12
  • 10
  • 9
    Thanks, exactly what I needed! :D Side note: a possible default needs to be type list / array, or Argparse will fail – Tarwin Jan 03 '19 at 07:17
  • Note that you can put more than three arguments for -i with this: you ought to check that and raise an error if the user adds more than 3. – drevicko Mar 31 '22 at 22:43
33

-i should be configured to accept 3 arguments and to use the append action.

>>> p = argparse.ArgumentParser()
>>> p.add_argument("-i", nargs=3, action='append')
_AppendAction(...)
>>> p.parse_args("-i a b c -i d e f -i g h i".split())
Namespace(i=[['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']])

To handle an optional value, you might try using a simple custom type. In this case, the argument to -i is a single comma-delimited string, with the number of splits limited to 2. You would need to post-process the values to ensure there are at least two values specified.

>>> p.add_argument("-i", type=lambda x: x.split(",", 2), action='append')
>>> print p.parse_args("-i a,b,c -i d,e -i g,h,i,j".split())
Namespace(i=[['a', 'b', 'c'], ['d', 'e'], ['g', 'h', 'i,j']])

For more control, define a custom action. This one extends the built-in _AppendAction (used by action='append'), but just does some range checking on the number of arguments given to -i.

class TwoOrThree(argparse._AppendAction):
    def __call__(self, parser, namespace, values, option_string=None):
        if not (2 <= len(values) <= 3):
            raise argparse.ArgumentError(self, "%s takes 2 or 3 values, %d given" % (option_string, len(values)))
        super(TwoOrThree, self).__call__(parser, namespace, values, option_string)

p.add_argument("-i", nargs='+', action=TwoOrThree)
Michael
  • 8,362
  • 6
  • 61
  • 88
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thanks for the hint! I had different issue, as append appends to the given default value not replace it and I needed a flat list. So I used the abstract base class `Action`. – TrueY Feb 13 '22 at 14:11
12

Adding with Other in this Thread.

If you use action='append' in add_argument() then you will get arguments in list(s) within a list every time you add the option.

As you liked:

[
   ["input1_url", "input1_name", "input1_other"],
   ["input2_url", "input2_name", "input2_other"],
   ["input3_url", "input3_name"]
]

But if anyone wants those arguments in the same list[], then use action='extend' instead of  action='append' in your code. This will give you those arguments in a single list.

[
  "input1_url", 
  "input1_name", 
  "input1_other", 
  "input2_url", 
  "input2_name", 
  "input2_other", 
  "input3_url", 
  "input3_name"
]
Shezan
  • 277
  • 2
  • 8