Optik: Callback Options
-----------------------

If Optik's built-in actions and types just don't fit the bill for you,
but it's not worth extending Optik to define your own actions or types,
you'll probably need to define a callback option.  Defining callback
options is quite easy; the tricky part is writing a good callback (the
function that is called when Optik encounters the option on the command
line).


Defining a callback option
--------------------------

As always, you can define a callback option either by directly
instantiating the Option class, or by using the add_option() method of
your OptionParser object.  The only option attribute you must specify is
'callback', the function to call::

  parser.add_option("-c", callback=my_callback)

Note that you supply a function object here -- so you must have already
defined a function 'my_callback()' when you define the callback option.
In this simple case, Optik knows nothing about the arguments the "-c"
option expects to take.  Usually, this means that the option doesn't
take any arguments -- the mere presence of "-c" on the command-line is
all it needs to know.  In some circumstances, though, you might want
your callback to consume an arbitrary number of command-line arguments.
This is where writing callbacks gets tricky; it's covered later in this
document.

There are several other option attributes that you can supply when you
define an option attribute:

type
    has its usual meaning: as with the "store" or "append" actions, it
    instructs Optik to consume one argument that must be convertible to
    'type'.  Rather than storing the value(s) anywhere, though, Optik
    converts it to 'type' and passes it to your callback function.

nargs
    also has its usual meaning: if it is supplied and > 1, Optik will
    consume 'nargs' arguments, each of which must be convertible to
    'type'.  It then passes a tuple of converted values to your
    callback.

callback_args
    a tuple of extra positional arguments to pass to the callback
    
callback_kwargs
    a dictionary of extra keyword arguments to pass to the callback


How callbacks are called
------------------------

All callbacks are called as follows::

  func(option : Option,
       opt : string,
       value : any,
       parser : OptionParser,
       *args, **kwargs)

where

option
    is the Option instance that's calling the callback

opt
    is the option string seen on the command-line that's triggering
    the callback.  (If an abbreviated long option was used, 'opt' will
    be the full, canonical option string -- e.g. if the user puts
    "--foo" on the command-line as an abbreviation for "--foobar",
    then 'opt' will be "--foobar".)

value
    is the argument to this option seen on the command-line.  Optik
    will only expect an argument if 'type' is set; the type of 'value'
    will be the type implied by the option's type (see "Option types"
    below).  If 'type' for this option is None (no argument expected),
    then 'value' will be None.  If nargs > 1, 'value' will be a tuple
    of values of the appropriate type.

parser
    is the OptionParser instance driving the whole thing, mainly
    useful because you can access some other interesting data through
    it, as instance attributes:

    parser.rargs
        the current remaining argument list, ie. with 'opt' (and
        'value', if any) removed, and only the arguments following
        them still there.  Feel free to modify parser.rargs,
        e.g. by consuming more arguments.
    
    parser.largs
        the current set of leftover arguments, ie. arguments that
        have been processed but have not been consumed as options
        (or arguments to options).  Feel free to modify
        parser.largs, e.g. by adding more arguments to it.
    
    parser.values
        the object where option values are by default stored.
        This is useful because it lets callbacks use the same
        mechanism as the rest of Optik for storing option values;
        you don't need to mess around with globals or closures.
        You can also access the value(s) of any options already
        encountered on the command-line.

args
    is a tuple of arbitrary positional arguments supplied via the
    callback_args option attribute.

kwargs
    is a dictionary of arbitrary keyword arguments supplied via
    callback_kwargs.

Since 'args' and 'kwargs' are optional (they are only passed if you
supply 'callback_args' and/or 'callback_kwargs' when you define your
callback option), the minimal callback function is::

  def my_callback (option, opt, value, parser):
      pass


Error handling
--------------

The callback function should raise OptionValueError if there are any
problems with the option or its argument(s).  Optik catches this and
terminates the program, printing the error message you supply to
stderr.  Your message should be clear, concise, accurate, and mention
the option at fault.  Otherwise, the user will have a hard time
figuring out what he did wrong.


Examples part 1: no arguments
-----------------------------

Here's an example of a callback option that takes no arguments, and
simply records that the option was seen::

  def record_foo_seen (option, opt, value, parser):
      parser.saw_foo = 1

  parser.add_option("--foo", action="callback", callback=record_foo_seen)

Of course, you could do that with the "store_true" action.  Here's a
slightly more interesting example: record the fact that "-a" is seen,
but blow up if it comes after "-b" in the command-line. ::

  def check_order (option, opt, value, parser):
      if parser.values.b:
          raise OptionValueError("can't use -a after -b")
      parser.values.a = 1
  [...]
  parser.add_option("-a", action="callback", callback=check_order)
  parser.add_option("-b", action="store_true", dest="b")

If you want to re-use this callback for several similar options (set a
flag, but blow up if "-b" has already been seen), it needs a bit of
work: the error message and the flag that it sets must be
generalized. ::

  def check_order (option, opt, value, parser):
      if parser.values.b:
          raise OptionValueError("can't use %s after -b" % opt)
      setattr(parser.values, option.dest, 1)
  [...]
  parser.add_option("-a", action="callback", callback=check_order, dest='a')
  parser.add_option("-b", action="store_true", dest="b")
  parser.add_option("-c", action="callback", callback=check_order, dest='c')

Of course, you could put any condition in there -- you're not limited
to checking the values of already-defined options.  For example, if
you have options that should not be called when the moon is full, all
you have to do is this::

  def check_moon (option, opt, value, parser):
      if is_full_moon():
          raise OptionValueError("%s option invalid when moon full" % opt)
      setattr(parser.values, option.dest, 1)
  [...]
  parser.add_option("--foo",
                    action="callback", callback=check_moon, dest="foo")

(The definition of is_full_moon() is left as an exercise for the
reader.)


Examples part 2: fixed arguments
--------------------------------

Things get slightly more interesting when you define callback options
that take a fixed number of arguments.  Specifying that a callback
option takes arguments is similar to defining a "store" or "append"
option: if you define 'type', then the option takes one argument that
must be convertible to that type; if you further define 'nargs', then
the option takes that many arguments.

Here's an example that just emulates the standard "store" action::

  def store_value (option, opt, value, parser):
      setattr(parser.values, option.dest, value)
  [...]
  parser.add_option("--foo",
                    action="callback", callback=store_value,
                    type="int", nargs=3, dest="foo")

Note that Optik takes care of consuming 3 arguments and converting
them to integers for you; all you have to do is store them.  (Or
whatever: obviously you don't need a callback for this example.  Use
your imagination!)


Examples part 3: variable arguments
-----------------------------------

Things get hairy when you want an option to take a variable number of
arguments.  For this case, you have to write a callback; Optik doesn't
provide any built-in capabilities for it.  And starting with Optik
1.2, you have to deal with the full-blown syntax for conventional Unix
command-line parsing.  (Previously, Optik took care of this for you,
but I got it wrong.  It was fixed in Optik 1.2, at the cost of making
this kind of callback more complex.)  In particular, callbacks have to
worry about bare "--" and "-" arguments; the convention is:

* bare "--", if not the argument to some option, causes command-line
  processing to halt and the "--" itself is lost

* bare "-" similarly causes command-line processing to halt, but
  the "-" itself is kept

* either "--" or "-" can be option arguments (this was broken in
  Optik 1.1 and earlier, and fixed in Optik 1.2)

If you want an option that takes a variable number of arguments, there
are several subtle, tricky issues to worry about.  The exact
implementation you choose will be based on which trade-offs you're
willing to make for your application (which is why Optik doesn't support
this sort of thing directly).

Nevertheless, here's a stab at a callback for an option with variable
arguments::

  def varargs (option, opt, value, parser):
      assert value is None
      done = 0
      value = []
      rargs = parser.rargs
      while rargs:
          arg = rargs[0]

          # Stop if we hit an arg like "--foo", "-a", "-fx", "--file=f",
          # etc.  Note that this also stops on "-3" or "-3.0", so if
          # your option takes numeric values, you will need to handle
          # this.
          if ((arg[:2] == "--" and len(arg) > 2) or
              (arg[:1] == "-" and len(arg) > 1 and arg[1] != "-")):
              break
          else:
              value.append(arg)
              del rargs[0]

       setattr(parser.values, option.dest, value)

  [...]
  parser.add_option("-c", "--callback",
                    action="callback", callback=varargs)

The main weakness with this particular implementation is that negative
numbers in the arguments following "-c" will be interpreted as further
options, rather than as arguments to "-c".  Fixing this is left as an
exercise for the reader.
