29

Is it possible to fetch multiple values for one option using getopt or optparse, as shown in the example below:

./hello_world -c arg1 arg2 arg3 -b arg4 arg5 arg6 arg7

Please note that the number of actual values for each option (-c, -b) could be either 1 or 100. I do not want to use: ./hello_world -c "arg1 arg2 arg3" -b "arg4 arg5 arg6 arg7"

It seems to me that this may not be possible (and perhaps in violation of POSIX), please correct me if I'm wrong.

I've seen examples where all the non-options at the end of the line (./hello_world -c arg1 -b arg1 arg2 arg3) can be gathered... but not for the first of multiple option.

I'd like my app to work on a wide range of platforms with different Python versions, so I've not looked at argparser.

Hooked
  • 84,485
  • 43
  • 192
  • 261
Open Grieves
  • 301
  • 1
  • 3
  • 6

7 Answers7

18

Yes, it can be done with optparse.

This is an example:

./test.py --categories=aaa --categories=bbb --categories ccc arg1 arg2 arg3

which prints:

arguments: ['arg1', 'arg2', 'arg3']
options: {'categories': ['aaa', 'bbb', 'ccc']}

Full working example below:

#!/usr/bin/env python

import os, sys
from optparse import OptionParser
from optparse import Option, OptionValueError

VERSION = '0.9.4'

class MultipleOption(Option):
    ACTIONS = Option.ACTIONS + ("extend",)
    STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",)
    TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",)
    ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",)

    def take_action(self, action, dest, opt, value, values, parser):
        if action == "extend":
            values.ensure_value(dest, []).append(value)
        else:
            Option.take_action(self, action, dest, opt, value, values, parser)


def main():
    PROG = os.path.basename(os.path.splitext(__file__)[0])
    long_commands = ('categories')
    short_commands = {'cat':'categories'}
    description = """Just a test"""
    parser = OptionParser(option_class=MultipleOption,
                          usage='usage: %prog [OPTIONS] COMMAND [BLOG_FILE]',
                          version='%s %s' % (PROG, VERSION),
                          description=description)
    parser.add_option('-c', '--categories', 
                      action="extend", type="string",
                      dest='categories', 
                      metavar='CATEGORIES', 
                      help='comma separated list of post categories')

    if len(sys.argv) == 1:
        parser.parse_args(['--help'])

    OPTIONS, args = parser.parse_args()
    print "arguments:", args
    print "options:", OPTIONS

if __name__ == '__main__':
    main()

More information at http://docs.python.org/library/optparse.html#adding-new-actions

Richard Gomes
  • 5,675
  • 2
  • 44
  • 50
14

Despite the claims of the other comments, this is possible with vanilla optparse, at least as of python 2.7. You just need to use action="append". From the docs:

parser.add_option("-t", "--tracks", action="append", type="int")

If -t3 is seen on the command-line, optparse does the equivalent of:

options.tracks = []
options.tracks.append(int("3"))

If, a little later on, --tracks=4 is seen, it does:

options.tracks.append(int("4"))
  • 1
    This is a more conventional solution to the problem. Passing multiple arguments to a single flag is a misleading practice and has a high probability of causing problems down the line. If you need IFS separated words passed to an *option*, I'd encourage users to quote the value and then split it within your code. Otherwise, stick with command arguments or repeated flags as suggested in this answer. – Ben Jul 06 '16 at 17:07
  • This requires arguments to be passed as "-c arg1 -c arg2 -c arg3" not "-c arg1 arg2 arg3" like the question requested. – pavon Dec 24 '20 at 03:18
7

Another option would be to define a separator and process it locally, like the options in the mount command.

For example, if , can be used as a separator:

...
args, _ = getopt.getopt(sys.argv[1:],'b:')
for flag, arg in args:
  if flag=='-b': all_arguments = arg.split(',')
...

$ ./test -b opt1,opt2,opt3

Same for space! But then your users have to quote it properly.

$ ./test -b 'opt1 opt2 opt3'
estani
  • 24,254
  • 2
  • 93
  • 76
7

Sorry to come late to the party but I just solved this with optparse using the nargs flag.

parser.add_option('-c','--categories', dest='Categories', nargs=4 )

http://docs.python.org/2/library/optparse.html#optparse.Option.nargs

It is also worth noting, that argparse (suggested by unutbu) is now part of the standard python distribution while optparse is deprecated.

Juan Carlos Moreno
  • 2,754
  • 3
  • 22
  • 21
6

Neither getopt nor optparse support this out of the box. In addition, in the default (GNU) mode, the additional arguments would be treated as interspersed args, i.e. become available as left-over arguments at the end of the processing.

The convention would be to require repeated mentioning of the same argument, i.e.

./hello_world -c arg1 -c arg2 -c arg3 -b arg4 -b arg5 -b arg6 -b arg7

This is will supported.

If you absolutely want to get it work the way you specify (i.e. both -b and -c extend until the next - argument or the end of the argument list), then you can hack something together based on optparse. Inherit from OptionParser, and override _process_short_opts. If it's one of your options, process it in the subclass, else forward to the base class.

Martin v. Löwis
  • 124,830
  • 17
  • 198
  • 235
  • 1
    Thank you for explaining the convention. That was what I thought.. The issue is that the number of values for each option makes using that convention non-user-friendly. I think I'm likely to end up iterating through sys.argv and handling things on my own. – Open Grieves Nov 05 '10 at 21:11
5

You can do this with the nargs parameter in argparse which comes with Python2.7, and downloadable here.

I think it is one of the improvements added to argparse which is not in optparse. So, unfortunately, I don't think there is a nice way to handle this with optparse or getopt (which is even older).

A quick and dirty solution might be to forgo optparse/getop/argparse and just parse sys.argv yourself.

Or, going in the opposite direction, you might consider packaging a frozen copy of argparse (~88K) (renamed something like argparse_static) with your program, and importing it like this:

try:
    import argparse
except ImportError:
    import argparse_static as argparse

That way, the program will use argparse if it is installed, and will use argparse_static if it is not. Best of all, you won't have to rewrite much code as argparse becomes standard.

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Thank you so much for two quick and in-depth answers. I did manage to write my own option parser that handles things. Downloading a static copy of argparse was not something that I've considered, but it's on the table now. – Open Grieves Nov 05 '10 at 21:01
  • My vote is with argparse. It is easier to get my head around than optparse was. (And I was one who hacked on optparse when it was Optik, before it made it into the standard library) – Terrel Shumway Nov 05 '10 at 22:18
3

An easier one:

make_option(
    "-c",
    "--city",
    dest="cities",
    action="append",
    default=[],
    help="specify cities",
)

Append action is the easiest solution for this problem.

Diego Navarro
  • 9,316
  • 3
  • 26
  • 33