Comma separated inputs instead of space separated inputs for argparse

前端 未结 5 966
灰色年华
灰色年华 2021-01-19 02:30

I\'m using argparse to receive inputs from the command line to run my script.

My current input string looks like this:

path> python &         


        
相关标签:
5条回答
  • 2021-01-19 03:10

    There is no such feature in argparse.

    Alternatives:

    • post-process the args namespace and split/parse the values manually
    • define a custom action and split/parse the values manually
    • define a custom type and split/parse the values manually
    • subclass ArgumentParser and customise ArgumentParser.convert_arg_line_to_args
    0 讨论(0)
  • 2021-01-19 03:11

    You 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)
    
    0 讨论(0)
  • 2021-01-19 03:17

    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'])
    
    0 讨论(0)
  • 2021-01-19 03:18

    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.

    Simple Version

    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.

    Better Version

    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.

    Other Options

    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.

    0 讨论(0)
  • 2021-01-19 03:18

    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.

    0 讨论(0)
提交回复
热议问题