I\'m using argparse
to receive inputs from the command line to run my script.
My current input string looks like this:
path> python &
There is no such feature in argparse
.
Alternatives:
args
namespace and split/parse the values manuallyaction
and split/parse the values manuallytype
and split/parse the values manuallyArgumentParser
and customise ArgumentParser.convert_arg_line_to_argsYou can use module shlex
to extract the parameters, then replace commas with spaces, and pass the results to argparse
for further processing:
comma_args = shlex.split("-t T1,T2,T3 -f F1,F2")
# ['-t', 'T1,T2,T3', '-f', 'F1,F2']
args = [x.replace(","," ") for x in comma_args]
# ['-t', 'T1 T2 T3', '-f', 'F1 F2']
parse_args(args)
If you're okay with space not comma separated, then it's builtin to argparse:
In [1]: from argparse import ArgumentParser
In [2]: parser = ArgumentParser()
In [3]: parser.add_argument('-a', nargs='+')
Out[3]: _StoreAction(option_strings=['-a'], dest='a', nargs='+', const=None,
default=None, type=None, choices=None, help=None, metavar=None)
In [4]: parser.parse_args(['-a', 'foo', 'bar'])
Out[4]: Namespace(a=['foo', 'bar'])
There are some useful answers here already, but I wanted a bit more: splitting on commas, validating values with choices
, and getting useful error messages, so below I offer a solution.
As a first pass, we can just pass in an appropriate function to the type
parameter:
>>> import argparse
>>> parser = argparse.ArgumentParser(prog='cmd')
>>> parser.add_argument('--foo', type=lambda arg: arg.split(','))
>>> parser.parse_args(['--foo', 'a,b,c'])
Namespace(foo=['a', 'b', 'c'])
But this doesn't work with choices
because it checks if the whole list is in choices and not each value:
>>> parser = argparse.ArgumentParser(prog='cmd')
>>> parser.add_argument('--foo', type=lambda arg: arg.split(','), choices=('a', 'b', 'c'))
>>> parser.parse_args(['--foo', 'a,b,c'])
usage: cmd [-h] [--foo {a,b,c}]
cmd: error: argument --foo: invalid choice: ['a', 'b', 'c'] (choose from 'a', 'b', 'c')
Setting nargs
to something like *
or +
checks each value, but only for space-separated arguments (e.g., --foo a b
) and not the comma-separated ones. It seems like there is no supported way to check if each value is in the choices if we produce the list ourselves. Therefore we need to raise errors ourselves via the type
parameter (as Shiplu Mokaddim partially implemented). Creating a custom Action class sounds promising as actions have access to the choices, but the action happens after the type
function applies and values are checked, so we still couldn't use the choices
parameter on add_argument()
for this purpose.
Here is a solution using a custom type
function. We this function to take a list of valid choices, but since the function for type conversion can only take an argument string, we need to wrap it in a class (and define the special __call__()
method) or a function closure. This solution uses the latter.
>>> def csvtype(choices):
... """Return a function that splits and checks comma-separated values."""
... def splitarg(arg):
... values = arg.split(',')
... for value in values:
... if value not in choices:
... raise argparse.ArgumentTypeError(
... 'invalid choice: {!r} (choose from {})'
... .format(value, ', '.join(map(repr, choices))))
... return values
... return splitarg
>>> parser = argparse.ArgumentParser(prog='cmd')
>>> parser.add_argument('--foo', type=csvtype(('a', 'b', 'c')))
>>> parser.parse_args(['--foo', 'a,b,c'])
Namespace(foo=['a', 'b', 'c'])
>>> parser.parse_args(['--foo', 'a,b,d'])
usage: cmd [-h] [-f F]
cmd: error: argument -f: invalid choice: 'd' (choose from 'a', 'b', 'c')
Notice that we get an appropriate error as well. For this, be sure to use argparse.ArgumentTypeError
and not argparse.ArgumentError
inside the function.
User wim suggested some other options not discussed above. I don't find those attractive for the following reasons:
Post-processing the argument after parsing means you have to do more work to get the error messages to be consistent with those from argparse
. Just raising argparse.ArgumentError
will lead to a stacktrace. Also, argparse
catches errors raised during parsing and alters them to specify the option that was used, which you'd otherwise need to do manually.
Subclassing ArgumentParser
is more work, and convert_arg_line_to_args()
is for reading arguments from a file, not the command line.
Here this comma-separated input is actually a different type. All you have to is to define the type. Here I define a custom type that does it.
class DelimiterSeperatedInput:
def __init__(self, item_type, separator=','):
self.item_type = item_type
self.separator = separator
def __call__(self, value):
values = []
try:
for val in value.split(self.separator):
typed_value = self.item_type(val)
values.append(typed_value)
except Exception:
raise ArgumentError("%s is not a valid argument" % value)
return values
parser.add_argument('-t', type=DelimiterSeperatedInput(str),
help='comma separated string values')
parser.add_argument('-f', type=DelimiterSeperatedInput(float, ":"),
help="colon separated floats')
This code may not work as-is, you might have to fix. But it was to give an idea.
Note: I could reduce the __call__
function body by using list
, map
etc. But then it wouldn't be very readable. Once you get the idea, you can do kinds of stuff with it.