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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.