Implementing variable number of option arguments

Greg Ward <[email protected]> Thu, 4 Nov 2004 21:55:56 -0500
Newsgroups gmane.comp.python.optik.user
Message-ID <[email protected]>
I had an idea the other night on how to implement options with optional
arguments in Optik.  Actually, it's more general than that: options with
a variable number of arguments.  The neat thing is, this could kill not
one, not two, but *three* birds with one stone:

  * optional option arguments

  * checking positional args for type and number (after option
    parsing is complete)

  * allowing variable types when nargs > 1

The first two are long-standing wish-list items that many people (myself
included) would like.  The latter was first brought up on this list
(AFAIK) just recently by Andrea Bocci, and I didn't think it very
important.  But I think it will fall out of my proposed implementation
pretty much for free, so what the heck.

First, the proposed semantics for optional option arguments is exactly
modelled on GNU getopt.  In particular:

  If an option takes optional arguments, any arguments that do not start
  with "-" will be consumed as arguments to that option.

(I didn't see this documented anywhere in the getopt(3) man page, but
it's what I've observed.  I've just checked in test/test_getopt.c if you
want to repeat the experiment yourself.)

This rule has interesting ramifications.  Consider three options:

   -a/--argh ARG     (required arg)
   -b/--bark         (no arg)
   -o/--oogh [ARG]   (optional arg)

In this command line:

  -a -- -b

getopt treats "--" as an argument to "-a", ie. it's equivalent to
"-a--", and "-b" is treated as an option.  But in this command
line:

  -o -- -b

the "--" has its usual meaning: "stop processing options now", and "-b"
is in the list of leftover positional args.  On reflection, that's
exactly the right behaviour, but it's one of the little gotchas that
optional option args will introduce.

Likewise, these two are quite different:

  -o foo -b
  -o - -b

In the former, "foo" is the argument to "-o"; in the latter, "-" becomes
a leftover positional argument.  The good news is that this is all
consistent with one simple rule, namely "options with optional args
don't consume following args that start with '-'".

Second, my proposed interface is:

  parser.add_option("-o", "--oogh", nargs=(0, 1), ...)

ie. nargs becomes a range (minargs, maxargs); the existing "nargs=N"
syntax remains as shorthand for "nargs=(N, N)".

So Optik's "optional option args" would actually mean "variable number
of option arguments", which if nothing else sounds less clunky.  If you
really want, you could write this:

  parser.add_option("--funky", nargs=(3, 5), ...)

and --funky would always consume the 3 following arguments, with an
error if there are less than three.  The subsequent two arguments would
only be consumed if they don't start with "-".  Eg. in

  --funky -4 x -- --blah -b

the arguments to --funky are ("-4", "-x", "--", "--blah").

I'm not completely sure what to do about the option value.  Clearly for
nargs=1 or (1, 1) -- the default if nargs is not specified -- it should
just be the single value.  Equally clearly, for nargs=(M, N) where N >=
M > 1, it should be a tuple of all values consumed.  Two tricky cases
remain:

  * nargs=(0, 1): this can never have more than one argument, so
    I would opt for None or the argument itself

  * nargs=(0, N) or (1, N): since these could have multiple values, I
    think it should always be a tuple -- possible an empty or singleton
    tuple

Hmmm... I think it's actually pretty simple.  Say nargs=(M, N).  If N=1
(the minimum), return None or the argument seen.  If N>1, return a
tuple.  That seems like a good balance between simple to understand,
simple to implement, and convenient to use.

Third, variable types:

  parser.add_option("--foo", nargs=3, type=(str, str, int))

means just what you think it does, ie. --foo takes a string, a string,
and an int, and this command line:

  --foo foo foo foo

would trigger an error.  If you don't specify enough types to cover your
bets, as in

  parser.add_option("--foo", nargs=3, type=str)

then the last type specified would be extended, so this example is
equivalent to type=(str, str, str).  Obviously, a single value stands in
for a singleton tuple.

Fourth, specifying leftover positional arguments.  I'm thinking along
these lines:

  parser = OptionParser(usage="%prog [options] [name1 [name2 [name3]]]")
  parser.add_option(...)
  [...]
  parser.parse_args(minargs=0, maxargs=3, type="string")

that is:

  * you specify constraints on positional args in the call to
    parse_args()

  * use explicit minargs, maxargs values instead of a nargs-style tuple
    -- it's much more common to take a range of positional args, after
    all, so I expect 0 < minargs < maxargs to be a fairly common case,
    unlike with options

  * 'type' is just like it is for an option, so you could specify mixed
    types with a tuple, where the last type is extended to cover all
    arguments seen

The current semantics (which would remain the default) are:

  parser.parse_args(minargs=0, maxargs=sys.maxint, type="string")

I'll see if I can cook up an implementation of this crazy scheme over
the next couple of days.  Looks like there might be a 1.5a3 after all.

(BTW, has anyone tried 1.5a2 yet?  Had any problems?)

        Greg
-- 
Greg Ward <[email protected]>                         http://www.gerg.ca/
"Question authority!"  "Oh yeah?  Says who?"


-------------------------------------------------------
This SF.Net email is sponsored by:
Sybase ASE Linux Express Edition - download now for FREE
LinuxWorld Reader's Choice Award Winner for best database on Linux.
http://ads.osdn.com/?ad_id=5588&alloc_id=12065&op=click