157

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"]] 
2
  • 1
    So why not associate the multiple input source arguments with that single option? Commented Mar 22, 2016 at 22:16
  • 1
    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 Commented Mar 22, 2016 at 22:17

5 Answers 5

136

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 ...]]].

Sign up to request clarification or add additional context in comments.

Comments

124

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']] 

2 Comments

Thanks, exactly what I needed! :D Side note: a possible default needs to be type list / array, or Argparse will fail
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.
35

-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) 

1 Comment

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.
21

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" ] 

Comments

1

I used combination of nargs=1 and action="append" for optional argument and got what I wanted

#!/usr/bin/env python3 """Argparse test""" from typing import List import argparse import sys import textwrap if __name__ == "__main__": descr = textwrap.dedent( """\ Demonstrate multiple optional attts """ ) usage = textwrap.dedent( """\ test.py [-d] [-s val] file file """ ) parser = argparse.ArgumentParser( prog="test.py", description=descr, usage=usage, formatter_class=argparse.RawTextHelpFormatter, ) parser.add_argument("-d", "--debug", action="store_true", help="set debug output") parser.add_argument("-s", "--skip", nargs = 1, action="append", help="skip") parser.add_argument("files", nargs=2, help="files") args = parser.parse_args() skip_list: List[str] = [] # Note: args.skip is a list of lists if args.skip is not None: for inner_list in args.skip: for val in inner_list: skip_list.append(val) print(f"debug={args.debug}") print(f"skip-list={skip_list}") print(f"files={args.files}") sys.exit() 

It works as expected

>./test.py file_a file_b debug=False skip-list=[] files=['file_a', 'file_b'] ~/python_test > ./test.py -d -s a -s b file_a file_b debug=True skip-list=['a', 'b'] files=['file_a', 'file_b'] 

1 Comment

Note that this answer ALSO solves the problem of disambiguating file_a into being a member of the files list instead the skip-list list.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.