Option Callbacks
================

When Optik's built-in actions and types aren't quite enough for your
needs, you have two choices: extend Optik or define a callback option.
Extending Optik is more general, but overkill for a lot of simple
cases.  Quite often a simple callback is all you need.

There are two steps to defining a callback option:

* define the option itself using the ``callback`` action

* write the callback; this is a function (or method) that
  takes at least four arguments, as described below


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

As always, the easiest way to define a callback option is by using the
``parser.add_option()`` method.  Apart from ``action``, the only option
attribute you must specify is ``callback``, the function to call::

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

``callback`` is a function (or other callable object), so you must have
already defined ``my_callback()`` when you create this callback option.
In this simple case, Optik doesn't even know if ``-c`` takes any
arguments, which usually means that the option takes no 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.

Optik always passes four particular arguments to your callback, and it
will only pass additional arguments if you specify them via
``callback_args`` and ``callback_kwargs``.  Thus, the minimal callback
function signature is::

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

The four arguments to a callback are described below.  

There are several other option attributes that you can supply when you
define a callback option:

``type``
    has its usual meaning: as with the ``store`` or ``append`` actions,
    it instructs Optik to consume one argument and convert it to
    ``type``.  Rather than storing the converted value(s) anywhere,
    though, Optik 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, opt_str, value, parser, *args, **kwargs)

where

``option``
    is the Option instance that's calling the callback

``opt_str``
    is the option string seen on the command-line that's triggering the
    callback.  (If an abbreviated long option was used, ``opt_str`` 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_str`` 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.  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
    its instance attributes:

    ``parser.largs``
        the current list of leftover arguments, ie. arguments that have
        been consumed but are neither options nor option arguments.
        Feel free to modify ``parser.largs``, e.g. by adding more
        arguments to it.  (This list will become ``args``, the second
        return value of ``parse_args()``.)
    
    ``parser.rargs``
        the current list of remaining arguments, ie. with ``opt_str`` and
        ``value`` (if applicable) removed, and only the arguments
        following them still there.  Feel free to modify
        ``parser.rargs``, e.g. by consuming more arguments.
    
    ``parser.values``
        the object where option values are by default stored (an
        instance of optik.OptionValues).  This 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 or modify 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``.


Raising errors in a callback
----------------------------

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.


Callback example 1: trivial callback
------------------------------------

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_str, value, parser):
      parser.saw_foo = True

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

Of course, you could do that with the ``store_true`` action.

Callback example 2: check option order
--------------------------------------

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_str, 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")

Callback example 3: check option order (generalized)
----------------------------------------------------

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_str, value, parser):
      if parser.values.b:
          raise OptionValueError("can't use %s after -b" % opt_str)
      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')

Callback example 4: check arbitrary condition
---------------------------------------------

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_str, value, parser):
      if is_moon_full():
          raise OptionValueError("%s option invalid when moon is full"
                                 % opt_str)
      setattr(parser.values, option.dest, 1)
  [...]
  parser.add_option("--foo",
                    action="callback", callback=check_moon, dest="foo")

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


Callback example 5: 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 ``nargs`` arguments.

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

  def store_value(option, opt_str, 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.)


Callback example 6: variable arguments
--------------------------------------

Things get hairy when you want an option to take a variable number of
arguments.  For this case, you must write a callback, as Optik doesn't
provide any built-in capabilities for it.  And you have to deal with
certain intricacies of conventional UNIX command-line parsing that Optik
normally handles for you.  In particular, callbacks should implement
the conventional rules for bare ``"--"`` and ``"-"`` arguments:

* either ``"--"`` or ``"-"`` can be option arguments

* bare ``"--"`` (if not the argument to some option): halt command-line
  processing and discard the ``"--"``

* bare ``"-"`` (if not the argument to some option): halt command-line
  processing but keep the ``"-"`` (append it to ``parser.largs``)

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 vararg_callback(option, opt_str, 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 (probably causing an error), rather than as arguments to
``"-c"``.  Fixing this is left as an exercise for the reader.

.. $Id: callbacks.txt 415 2004-09-30 02:26:17Z greg $
