appeal 0.6.3
A powerful & Pythonic command-line parsing library.  Give your program Appeal!
Requires Python: >=3.6

![## Appeal](https://raw.githubusercontent.com/larryhastings/appeal/master/resources/images/appeal.logo.png)

![## Give your program Appeal!](https://raw.githubusercontent.com/larryhastings/appeal/master/resources/images/give.your.program.appeal.png)

##### Copyright 2021-2023 by Larry Hastings

[![# test badge](https://img.shields.io/github/actions/workflow/status/larryhastings/appeal/test.yml?branch=master&label=test)](https://github.com/larryhastings/appeal/actions/workflows/test.yml) [![# python versions badge](https://img.shields.io/pypi/pyversions/appeal.svg?logo=python&logoColor=FBE072)](https://pypi.org/project/appeal/)


## Quickstart

```Python
import appeal
import sys

app = appeal.Appeal()

@app.command()
def hello(name):
    print(f"Hello, {name}!")

app.main()
```

Here's a simple ``fgrep`` utility:

```Python
import appeal
import sys

app = appeal.Appeal()

@app.command()
def fgrep(pattern, *files, ignore_case=False):
    if not files:
        files = ['-']
    print_file = len(files) > 1
    if ignore_case:
        pattern = pattern.lower()
    for file in files:
        if file == "-":
            f = sys.stdin
        else:
            f = open(file, "rt")
        for line in f:
            if ignore_case:
                match = pattern in line.lower()
            else:
                match = pattern in line
            if match:
                if print_file:
                    print(file + ": ", end="")
                print(line.rstrip())
        if file != "-":
            f.close()


if __name__ == "__main__":
    app.main()
```


## Overview

Appeal is a command-line argument processing library for
Python, like `argparse`, `optparse`, `getopt`,
`docopt`, `Typer`, and `click`.  But Appeal takes a
refreshing new approach.

Other libraries have complicated, cumbersome interfaces
that force you to repeat yourself over and over.
Appeal leverages Python's rich function call interface,
making it effortless to define your command-line interface.
You write Python functions, and Appeal translates them into
command-line options and arguments.

Appeal provides amazing power and flexibility--but it's
also intuitive, because it mirrors Python itself.
If you understand how to write Python functions,
you're already halfway to understanding Appeal!

Appeal has only one dependency,
[my **big** library.](https://github.com/larryhastings/big)

Appeal is currently only supported for POSIX platforms
(UNIX, Linux, BSD, OS X, etc).  It might work on Windows
but this has not yet been tested.

### A New And Appealing Approach

Appeal isn't like other command-line parsing libraries.
In fact, you really shouldn't think of Appeal as a
"command-line parsing library" per se. And, although you
work with Appeal by passing in functions for Appeal to call,
you shouldn't think of these functions as "callbacks".

Appeal lets you design *APIs* callable from the command-line.
It's just like any other Python library API--except that
the caller calls you from the command-line instead of from
Python.  Appeal is the mechanism converting between these two
domains: it translates your API into command-line semantics,
then translates the user's command-line back into calls to your API.

This raises another good point: the API you build using Appeal
also often makes for a very nice *automation API,* allowing
your program to also be used as a library by other programs
with minimal effort.


## Basics

### Taxonomy

Let's start by establishing the terminology we'll use
for command-lines, based on command-line idioms established
by POSIX and by popular programs.  Here's a sample
command-line, illustrating all the various types of things
you might ever see:

    % ./script.py --debug add --flag ro -v -xz myfile.txt
      ^           ^       ^   ^      ^  ^  ^   ^
      |           |       |   |      |  |  |   |
      |           |       |   |      |  |  |   argument
      |           |       |   |      |  |  |
      |           |       |   |      |  |  multiple short options
      |           |       |   |      |  |
      |           |       |   |      |  short option
      |           |       |   |      |
      |           |       |   |      oparg
      |           |       |   |
      |           |       |   long option
      |           |       |
      |           |       command
      |           |
      |           global long option
      |
      program name

Command-lines are a sequence of strings separated by
whitespace.  The meaning of each string can depend
both on the position of the string and the characters
in the string itself.

An *argument* is any whitespace-delimited string on the
command-line that doesn't start with a `-` (minus sign).
Unless it's an *oparg*--which we'll talk about in a minute--the
meaning of an argument is defined by its position.  For example,
if you ran:

    fgrep WM_CREATE window.c

`WM_CREATE` and `window.c`
would be *arguments;* the first argument, `WM_CREATE`,
would be the string you wanted to search for, and `window.c`
would be the name of the file you wanted to search.

A *command* is a special kind of argument some programs
use to specify what function you want the program to perform.
A good example of a program that uses commands is `git`;
when you run `git add` or `git commit`, `add` and `commit`
are both *commands.*  The command is always the first
argument to a program that uses them.

If a string on the command-line starts with a `-` (minus
sign), that's an *option*.  There are two styles of
option: *short options* and *long options.*

*Short options* start with a single dash, `-`.  This is
followed by one or more individual characters, which
are the short option strings.  In the above example,
we specify two sets of short options: the first is `-v`,
the second is `-xz`.  You can combine options togther,
and it's the same as specifying them separately.  We
could have said `-vxz`, or `-v -x -z`; these both do
the same thing.  When we talk about short options, we
say the word "dash" followed by the letter.  For example,
`-v` would be pronounced "dash v".

*Long options* start with two dashes, `--`.  Everything
after the two dashes is the name of the option.  In the
above example, we can see one long option, `--flag`.
Again, when we talk about long options, we say the
dashes out loud, followed by the words from the option.
For example, `--flag` would be pronounced "dash dash flag".

Both types of options can optionally take one (or more)
arguments of their own.  An argument to an option is
called an *oparg.*  In the above example, the long option
`--flag` takes the oparg `ro`.

Finally, there are *global options* and *command
options.*  Global options apply to the entire
program, are always available, and are specified
*before* the command.  Command options are
command-specific, and appear *after* the command.
Global options can be long options or short options;
command options can be long options or short options, too.


### Remapping Python To The Command-Line

Now let's consider a Python function call:

```Python
#      positional parameters |  keyword-only parameters
#            |        |      |      |
#            v        v      |      v
def fgrep(pattern, filename, *, ignore_case=False):
    ...
```

We can draw some similarities between Python
function calls and command-lines.

For example, they both support arguments where
position is significant.  A command-line *argument*
is similar to a Python function *positional*
parameter, in that they're both identified by
position.

Python function calls and command-lines also
both support arguments identified by name.
A command-line *option* is similar to a Python
*keyword-only* parameter.

> In Python, all parameters after `*args` or `*` are
> keyword-only parameters.  You can only pass in arguments
> to those parameters by name.  If you called `fgrep`
> yourself, you could only pass in a value for `ignore_case`
> like this:
>
>     result = fgrep(p, f, ignore_case=True)

This leads us to the fundamental concept behind Appeal.
With Appeal, you write a Python function, and tell
Appeal that it represents a *command.*  Appeal
examines the function, translating its parameters into
command-line features.  Positional parameters become
command-line arguments, and keyword-only parameters
become options.

(Technically, Appeal translates both *positional parameters*
and *positional-or-keyword parameters* into arguments.
For the sake of clarity and consiseness, I'll always
refer to these collectively as *positional parameters.)*


## Hello, World!

Let's see Appeal in action, with our first example.
In all our examples we're going to assume your program
is called `script.py`.  Let's say `script.py` looked like
this:

```Python
import appeal
app = appeal.Appeal()

@app.command()
def hello(name):
    print(f"Hello, {name}!")

app.main()
```

If you now ran `python3 script.py help hello`, you'd
see usage information for your `hello` command.
It'd start like this:

    usage: script.py hello name

Already, a lot has happened!  Let's go over it piece by piece:

* We created an `Appeal` object called `app`.
  This object will handle processing the command-line
  and calling the appropriate command function.
* We decorated the function `hello()` with `@app.command()`,
  a method call on our Appeal object.
  This tells Appeal that `hello()` should be a
  *command*, using the name of the function as the
  command string, and translating the function's
  parameters into the command-line parameters.
  So our command-line command is called `hello`.
  We call a function decorated with `@app.command()`
  a *command function.*
* Our `hello()` command function takes one positional
  parameters, `name`.  Therefore, our `hello` command
  on the command-line takes one positional argument,
  which we identify as `name` in the usage string.
* Appeal also automatically created simple help for our
  program, displaying *usage* information.  Usage shows
  you what command-line options and arguments the command
  will accept.

So!  If you ran this command at the command-line:

    % python3 script.py hello world

Appeal would call your `hello()` function like this:

```Python
hello('world')
```

and you'd be rewarded with:

    Hello, world!

The return value from your command function is the return
code for your program.  If you return `None` or `0`, that's
considered success; returning a non-zero integer indicates
failure.  (And if your function exits without a return
statement, Python behaves as if your function ended with
`return None`.)


## Default Values And `*args`

Let's change up our example, and add an optional parameter:

```Python
import appeal
app = appeal.Appeal()

@app.command()
def fgrep(pattern, filename=None):
    print(f"fgrep {pattern} {filename}")

app.main()
```

Now our command is called `fgrep`, and it takes two parameters.
The second one, `filename`, is optional, with a default value of `None`.

You can of course specify both parameters yourself.  Running this:

    % python3 script.py fgrep WM_CREATE window.c

results in Appeal calling your `fgrep()` function like this:

```Python
fgrep('WM_CREATE', 'window.c')
```

But you can also omit the `filename` parameter.
If you run this command at the command-line:

    % python3 script.py fgrep WM_CREATE

Appeal would call `fgrep()` like this:

```Python
fgrep('WM_CREATE', None)
```

Actually that's not 100% accurate.  When Appeal
builds the arguments to call your `fgrep()` function,
it only passes in the arguments you passed in on the
command-line.  So actually Appeal calls your `fgrep()`
function like this:

```Python
fgrep('WM_CREATE')
```

And it's Python that sets the `filename` parameter to `None`.

What else can Appeal command functions do?  Well, they can
have a `*args` parameter. Naturally, a command function that
takes `*args` (internally called a *var_positional*
parameter) can accept as many positional arguments as the
user wants to supply.  Here's a demonstration:

```Python
import appeal
app = appeal.Appeal()

@app.command()
def fgrep(pattern, *filenames):
    print(f"fgrep {pattern} {filenames}")

app.main()
```

Now the user could pass in no filenames, one filename,
fifty filenames--as many as they want!  They'd all be
collected in a tuple and passed in to `fgrep()` in the
`filenames` parameter.


## Options, Opargs, And Keyword-Only Parameters

Now let's examine what Appeal does with keyword-only
parameters.  Let's add three keyword-only parameters
to our example:

```Python
import appeal
app = appeal.Appeal()

@app.command()
def fgrep(pattern, *filenames, color="", number=0, ignore_case=False):
    print(f"fgrep {pattern} {filenames} {color!r} {number} {ignore_case}")

app.main()
```

Now the `fgrep` command-line usage looks like this:

    usage: script.py fgrep [-c|--color] [-n|--number int] [-i|--ignore-case] pattern [filenames]...

Again, a lot just happened.

First, I'll remind you, keyword-only parameters
are presented as options on the command-line.
Appeal automatically took each keyword-only parameter,
added `'--'` to the front of the parameter name,
and turned that into an option.  (Also, if the parameter
name has any underscores, Appeal turns those into dashes.)

Second, Appeal also automatically uses the first letter of a
keyword-only argument as a short option.  So the
`color` keyword-only parameter becomes both the `--color`
*and* `-c` options.  When running your program, the user
can use `-c` or `--color` interchangably.  The same goes
for `-i` and `--ignore_case`, and for `-n` and `--number`.

(What if you have two keyword-only parameters that start
with the same letter?  The first one gets the short option.
If we added a keyword-only parameter named `credit` to the
end of `fgrep()`'s parameter list, Appeal would map `color`
to `--color` and `-c`, but only map `credit` to `--credit`.)

Third, options are *always optional.*
(As a pedantic wag might put it--"the clue's right there in the name.")
Therefore, in Appeal, keyword-only
parameters to command functions must *always* have a
default value.  (Python programmers usually have default
values for their keyword-only parameters anyway, so this
requirement isn't a big deal.)

Fourth, notice that `--color` takes an argument, or *oparg.*
Appeal noticed that the `color` parameter had a default
value of `""`--its default value is a `str`.
So Appeal infers that you want the user to supply an oparg
to `--color`.  If the user specifies `--color` on the
command-line, it must be followed by an oparg, and Appeal
will take the string off the command-line and pass it
straight into the `color` parameter.

Fifth, `--number` also takes an oparg, but it has a default of `0`.
Appeal infers from that that `--number` should be an `int`.
Appeal automatically converts the string from the command-line
into a Python object for you, using the type of the default value.
(Appeal did that for `--color` too--except `--color` takes a str,
so no conversion is necessary.)  When the user provides an oparg
to `--number` on the command-line, it must be followed by an
oparg; Appeal will take that oparg, pass it in to `int`, then take
the return value from `int` and pass it in to the `number` parameter.

Finally, `ignore_case` has a default value of `False`.
Boolean values for options are a special case: they don't
take an oparg. All they do is negate the default value.
So if the user specifies `-i` once on the command-line,
Appeal would pass `True` in to the `ignore_case` parameter.

(By the way, a default value of `None` is a second
special case.  If a positional or keyword-only parameter
has a default value of `None`, Appeal behaves as if the
type of the default is `str`.  It consumes an argument
or oparg from the command-line and passes it in unchanged
to that parameter.)

Let's put it all together!  If you ran this command at the command-line:

    % python3 script.py fgrep -i --number 3 --color blue WM_CREATE window.c

Appeal would call `fgrep()` like this:

```Python
fgrep('WM_CREATE', 'window.c', color='blue', number=3, ignore_case=True)
```

And if you ran this command at the command-line:

    % python3 script.py fgrep --color green boogaloo

Appeal would call `fgrep()` like this:

```Python
fgrep('boogaloo', color='green')
```


## The Global Command, Subcommands, And The Default Command

Many programs that support "commands" also have
"global options".  Global options are options
specified on the command-line *before* the command.
For example, in the example command-line at the top
of this document, `script.py` takes a `--debug`
option specified before the command--which makes it
a "global option".

Appeal supports global options, too.  It's simple:
write your command function like normal, but
instead of decorating it with `Appeal.command()`, decorate
it with `Appeal.global_command()`.  Appeal will process all
those options before the command, and call your global
command function.

`Appeal.global_command()` also gets used for programs that
don't use "commands".  Although the "command" command-line
paradigm is popular these days, most programs don't bother
with them.  For example, `ls`, `grep`, and... hey! `python`
itself!  None of these programs support commands, but they
all support command-line arguments and options.

Naturally, Appeal supports this behavior.  Simply decorate
one function with `Appeal.global_command()` and don't add
any command functions.

On the flip side of this coin, Appeal also supports
*subcommands*.  This is a common feature of command-line
parsing libraries, though it's rarely-used in practice.
The idea is, your command can *itself* be followed by
another command.

To add a subcommand to your Appeal instance, just
decorate your command function with two chained
command calls, specifying the name of the existing
command in the first call, like so:

```Python
@app.command()
def db(...):
    ...

@app.command("db").command()
def deploy(...):
    ...
```

This adds a `deploy` subcommand under the `db` command.
So now the whole command-line looks something like this:

    script.py [global arguments and options] db [db arguments and options] deploy [deploy arguments and options]

Finally, what should Appeal do if your program
takes commands, but the user doesn't supply one?
That's what the *default command* is for.  The
default command is a command function Appeal will
run for you if your Appeal instance has commands,
and the user doesn't supply one.  For example,
if `script.py` has ten different commands, but the
user just runs

    script.py

without any arguments, Appeal would run the default
command.

If you don't specify a default command, Appeal has
a built-in default *default command*.  The default *default
command* raises a usage error, which means it prints basic help
information and exits.

To specify your own default command, just decorate a
command function with the `Appeal.default_command()` decorator.
For example, if you wanted your program to run the `status`
command when the user didn't specify a command, you could
do this:

```Python
@app.default_command()
def default():
    return status()
```

Notice that the default command doesn't take any arguments
or options.  It simply can't accept any, by definition.

(If the user specified options
without a command, they'd be considered "global options"
and would be processed by the global command.  And if the
user specified an argument, that would automatically be the
name of the command to run.)

And yes, subcommands can have a default command too:

```Python
@app.command('db').default_command()
def db_default():
    return db_status()
```

## Annotations And Introspection

Python 3 supports annotations for function parameters, meant
to conceptually represent types.  Appeal supports annotations
too; they explicitly tell Appeal what type of object a parameter
requires.  For example:

```Python
import appeal
app = appeal.Appeal()

@app.command()
def fgrep(pattern, *filenames, id:float=None):
    print(f"fgrep {pattern} {filenames} {id}")

app.main()
```

Here `id` has a default value of `None`, but it also has
an explicit annotation of `float`.   If the user uses `--id`
on the command-line, it must be followed by an oparg,
which Appeal will convert to a Python object by calling `float`.
(And, as you can see, the annotation and the type of the default
don't *necessarily* have to agree... although it's usually a
good idea.)

Although annotations are *meant* to represent types, Appeal
actually accepts any callable--it can be a type, or a
user-defined class, or just a regular function.  Appeal
calls these annotations *converters.*

Here's how Appeal decides on the converter for a parameter,
from highest-priority to lowest-priority:

* If the signature for that parameter has an annotation,
  Appeal uses the annotation as the converter.
* If the signature for that parameter *doesn't* have an
  annotation, but *does* have a default value, Appeal
  will use `type(default)` as the converter in most cases.
  The exceptions:
  - If `type(default)` is `NoneType`, Appeal will use `str`
    instead.
  - If `type(default)` is `bool`, and the parameter is a
    keyword-only parameter, Appeal will use a special internal
    class that implements the special-case "negate the default"
    behavior for options with boolean default values.
* If the signature for that parameter lacks both an annotation
  *and* a default value, Appeal uses `str` as the converter.

Converters are surprisingly flexible.
For example, Appeal will introspect the converter for a
keyword-only parameter and map all its positional arguments
into opargs.  That's how Appeal supports options that take
*multiple opargs:* you simply annotate the keyword-only
parameter with a converter that takes *multiple arguments.*
Appeal will also pay attention to the annotations for the
converter's own arguments, and use those to convert the
strings from the command-line into Python objects.

Let's tie it all together with another example:

```Python
import appeal
app = appeal.Appeal()

def int_and_float(integer: int, real: float):
    return [integer*3, real*5]

@app.command()
def fgrep(pattern, *filenames, position:int_and_float=(0, 0.0)):
    print(f"fgrep {pattern} {filenames} {position}")

app.main()
```

Here, Appeal would introspect `fgrep()`, then also
introspect `int_and_float()`.  The resulting usage
string would now look like this:

    usage: script.py fgrep [-p|--position integer real] pattern [filenames]...

`--position` takes *two* opargs.  Appeal would
call `int` on the first one and `float` on the second
one.  It would then call `int_and_float()` with those
values, and the return value of `int_and_float()` would
be passed in to the `position` parameter on `fgrep()`.

So now if you ran:

    % python3 script.py fgrep -p 2 13 funkyfresh

Appeal would call:

```Python
fgrep('funkyfresh', position=[6, 65.0])
```

Finally, let's change the example to demonstrate something
else: although converters can be any callable, user-defined
classes work fine too.  And Appeal can correctly infer the
type based on the default value for any type.  So consider
this example:

```Python
import appeal
app = appeal.Appeal()

class IntAndFloat:
    def __init__(self, integer: int, real: float):
        self.integer = integer * 3
        self.real = real * 5

    def __repr__(self):
        return f"<IntAndFloat {self.integer} {self.real}>"

@app.command()
def fgrep(pattern, *filenames, position=IntAndFloat(0, 0.0)):
    print(f"fgrep {pattern} {filenames} {position}")

app.main()
```

This example behaves essentially the same as the previous example
in this section, except the formatting of `position` is slightly
different.  But the command-line usage is exactly the same!
Appeal inferred the converter for `position` based on the type
of its default value, then introspected that type to determine
how many opargs it should consume from the command-line and how
to convert them.


> **An important note about annotations**
>
> If you use static type analysis in your project,
> your static type analyzer may not enjoy analyzing Python
> code using Appeal.  Static type analyzers are designed
> to understand "type hints", a means of specifying static
> type information introduced in Python 3.5 with the
> `typing` module.  But Appeal doesn't use type hints,
> and there are some ways Appeal uses annotations that
> static type analyzers may not like.
>
> Fortunately, there are ways to get static type analyzers
> to work alongside Appeal.
>
> First, you can decorate your Appeal command functions
> and converters with `@typing.no_type_check()`.  This should
> only be necessary if you use functions as annotations;
> if you only ever use types and classes, this shouldn't be
> necessary.
>
> Second, if you're using Python 3.9 or newer, you can use
> `typing.Annotated` with your annotations.  `typing.Annotated`
> allows you to specify an ordered list of values, and static
> type hints only ever use the *first* value.  Appeal also
> handles `typing.Annotated`, but Appeal only ever uses the
> *last* value.  This makes it easy--you can have both types
> of annotations, side by side, and both static type checkers
> and Appeal are perfectly happy.


## Converter Flexibility

You can use almost any function as an annotation...
within reason.  Appeal will introspect your annotation,
determine its input parameters, and call it to convert
the command-line argument into the argument it passes
in to your command function.

For example, what if you wanted an option that accepted
a string which gets broken up based on a delimiter substring?
This is a common idiom for `configure` scripts on UNIX-like
platforms; for example,
[Python's own `configure` script](https://github.com/python/cpython/blob/3.9/configure)
supports this option:

    --with-dbmliborder=db1:db2:...

Happily that's easy to do in Appeal.  Just write a converter
function that accepts a string, breaks it into substrings
however you like, and returns the list.

Although... you don't need to bother!  Appeal also provides
a converter that does it for you, called `appeal.split()`.
You pass in as many delimiter strings as you want, and
`appeal.split()` will split the command-line across all of
them.  (If you don't specify any delimiters, `appeal.split()`
will split at every whitespace character.)


## Specifying An Option More Than Once

One thing you might have noticed by now: the interfaces
you've seen only allow Appeal to handle command-lines
where an option can be specified either zero times or
one time.  What if you want the user to be able to
specify an option three times?  Or ten?  That's what the
`MultiOption` class is for.  `MultiOption` objects
are converters that allow options to be specified
multiple times.

`MultiOption` isn't useful by itself; it's only an
abstract base class.  To make use of it you'll
need to use a subclass--or create your own.

This time, let's start with some examples.  Appeal
provides three useful subclasses of `MultiOption`:
`counter`, `accumulator`, and `mapping`.

First, let's look at `counter`.  `counter`
simply counts the number of times an option is
specified on the command-line.  This is a somewhat
common idiom for "verbose" options; a program
that supports `-v` to mean *verbose* may allow
you to specify `-v` more than once to make
it *more* verbose.  Here's how you'd do that
with Appeal:

```Python
import appeal
app = appeal.Appeal()

@app.command()
def fgrep(*, verbose:appeal.counter()=0):
    print(f"fgrep verbose={verbose!r}")

app.main()
```

If the user ran

    % python3 script.py fgrep

Appeal would call

```Python
fgrep()
```

allowing Python to pass in the default value of `0` to `verbose`.
And if the user ran

    % python3 script.py fgrep -v --verbose -v

Appeal would call

```Python
fgrep(verbose=3)
```

`accumulator` handles options that take a single oparg.
It remembers them all and returns them in a single array.
Like so:

```Python
import appeal
app = appeal.Appeal()

@app.command()
def fgrep(*, pattern:appeal.accumulator=[]):
    print(f"fgrep pattern={pattern!r}")

app.main()
```

If the user ran

    % python3 script.py fgrep --pattern three -p four --pattern fiv5

Appeal would call

```Python
fgrep(pattern=['three', 'four', 'fiv5'])
```

What if you don't want strings, but another type?  Using crazy
science magic from the future, `accumulator` is actually
parameterized.  You can say:

```Python
import appeal
app = appeal.Appeal()

@app.command()
def fgrep(*, pattern:appeal.accumulator[int]=[]):
    print(f"fgrep pattern={pattern!r}")

app.main()
```

and now the opargs to `--pattern` will all be converted using int.

You can even specify multiple types as arguments to the
parameterized version of `accumulator`, separated by commas.
The option will then require multiple opargs and convert
them to the types specified.

`mapping` is like `accumulator` except it returns a
`dict` instead of a `list`.  An option annotated with `mapping()`
consumes *two* positional arguments from the command-line;
the first one is the key, the second one is the value.
(You can also parameterize `mapping` the same way you parameterize
`accumulator`, though you can only specify exactly two types.)


Of course, you can also subclass `MultiOption` to make your own
converter classes with custom behavior. `MultiOption` subclasses
can override these three methods:

```Python
class MultiOption:

    def init(self, default):
        ...

    def option(self, ...):
        ...

    def render(self):
        ...
```

Well, actually, subclasses are *required* to override
`option()` and `render()`.  But `init()` is optional.

If you then specify a subclass of `MultiOption` as an
annotation on a keyword-only parameter of an
Appeal command function, several things happen:

* If that option is specified one or more times on
  the command-line, Appeal will instantiate exactly
  one of these objects and call its `init()` method.
* Every time the user specifies that option on
  the command-line, Appeal will call the `option()`
  method on the object.
* After finishing processing the command-line,
  Appeal will call the `render()` method on the
  object, and pass the value it returns as the
  argument to that keyword-only parameter.

The most powerful part of this interface: you can
redefine `option()` to suit your needs--it supports
the same sort of polymorphism as annotations do.
Appeal will introspect your `option()` method to
determine how many opargs to consume from the
command-line, and how to convert them.

Let's demonstrate all this with another example.
If you want your option to take two opargs,
with one being an `int` and the other being
a `float`, you would define `option()` in your
subclass as:

```Python
class MyMultiOption(appeal.MultiOption):

    def option(self, a:int, b:float):
        ....
```

Every time the user specified your option,
it would take two opargs, and they would be
converted into an `int` and a `float` before
calling your `option()` method.  It's up to
you to decide how to store them, and how to
render them into a single value returned
by your `render()` method.

`MultiOption` is a subclass of a general
`Option` class.  `Option` behaves identically
to `MultiOption`, except it only permits
specifying the option once on the command-line,
which means it will only your `option()`
method once.
You usually don't need to bother with making subclasses
of `Option`--it's usually better to just use a class
directly, like our `class IntAndFloat` example.
The only feature you get by subclassing `Option` is,
you get the default value for the parameter passed in
to your constructor.

(The downside of subclassing `Option` and `MultiOption`
is that it makes exporting your Appeal API as an automation
API a little less convenient for the user, because your
users will have to construct these objects and feed
values into them by calling the `option` method.)


## Data Validation

What if you want to restrict the data the user provides
on the command-line?  That's simple, just use a converter!
Appeal provides a couple sample converters for data validation,
but it's easy to write your own.

The classic example is a parameter where you can only use one
of a list of values.  For that, you can use Appeal's `validate()`
converter.  For example, this command restricts the `direction`
parameter to one of six canonical directions:

```Python
import appeal
app = appeal.Appeal()

@app.command()
def go(direction:appeal.validate('up', 'down', 'left', 'right', 'forward', 'back')):
    print(f"go direction={direction!r}")

app.main()
```

You can pass in an explicit type using a `type=`
named argument to `validate()`; if you omit it,
it uses the type of the first argument.

Appeal also has a built-in range validator
called `validate_range()`.  It takes `start`
and `stop` arguments the same way Python's
`range()` function does. Note that `validate_range()`
differs from Python's `range()` in one subtle way:
values *equal* to `stop` are allowed.

If you prefer, you can "clamp"
the value the user passed in to the range,
by supplying the argument `clamp=True` to
`validate_range()`. In that case, if the value
the user specifies is outside the range, `validate_range()`
will return the closest value of either `start` or `stop`.

(That's why `validate_range()` allows the
value to be *equal to* `stop`.  `clamp` would
be annoying to use if `stop` itself was an
illegal value--particularly if the types
were floats.)

Appeal validation functions are straightforward to write.
So, if these are insufficient to your needs,
you can easily write your own.  Take a look
at the implementations of `validate()` and
`validate_range()` inside Appeal to see one way to do it!


## Multiple Options For The Same Parameter

Some programs have a set of options on their
command-line that are mutually exclusive.  Consider
this simple-minded command-line:

    go [--north|--south|--east|--west]

That is, you want the user to be able to "go" in
one of those four directions, but *only* one.
How would you do that in Appeal?

Easy.  You simply define multiple options that
write to the same parameter.  All the behavior
you've seen so far is using the *default* way of
mapping keyword-only parameters to options.  But
actually Appeal allows you to make your own mappings.
You can map a parameter as many ways as you want,
even using different converters!

To manually define your own options, use the `Appeal.option()`
method on your Appeal instance.  It's a decorator you
apply to your command function.  The first argument is
the name of the parameter you want the option to write
to.  After that is one or more options you want to
map to this parameter.  `Appeal.option()` also takes
`default` and `annotation` keyword-only parameters,
allowing you to specify respectively the default value or
annotation for this option.

Here's a simple example of how to implement the above `go`
command with Appeal:

```Python
import appeal
app = appeal.Appeal()

@app.command()
@app.option("direction", "--north", annotation=lambda: "north")
@app.option("direction", "--south", annotation=lambda: "south")
@app.option("direction", "--east",  annotation=lambda: "east")
@app.option("direction", "--west",  annotation=lambda: "west")
def go(*, direction='north'):
    print(f"go direction={direction!r}")

app.main()
```

All these annotations return a string.  But actually you can
return any type you want--and you can even map multiple
annotations that return different types to the same parameter.
You can even annotate with a `MultiOption` to allow specifying
that option multiple times!

Note that, whenever you use the `option()` decorator
to map your own options onto a parameter, Appeal won't add
its default options for that parameter.  It'll only have
the options you explicitly set.  Which means, for example,
that in the sample code above, there aren't any short options
for the options we created.  `-n` won't work, only `--north`.

One final thing.  Your command function can accept `**kwargs`
too.  The only things that will go into it are options you
create with `Appeal.option()`, which map to parameters that
don't otherwise exist.


## Recursive Converters

You already know that you can pass in a converter that takes
multiple arguments, and Appeal will consume multiple arguments
from the command-line to fill it.  And if the arguments to that
converter have annotations, Appeal will call those functions to
convert the command-line argument into the type your converter
wants.

But what if you did... *this?*

```Python
import appeal
app = appeal.Appeal()

def int_float(i: int, f: float):
    return (i, f)

def my_converter(i_f: int_float, s: str):
    return [i_f, s]

@app.command()
def recurse(a:str, b:my_converter=[(0, 0), '']):
    print(f"recurse a={a!r} b={b!r}")

app.main()
```

The `my_converter()` parameter `i_f` is a positional
parameter with an annotation that, itself,
*takes two positional parameters.*

Would it surprise you to know--yes, it actually works!

Converters have been fully recursive this *whole time.*
Actually this fact has been hiding in plain sight
all along--all the examples using `int_and_float()` are recursive
too, because `int_and_float()` has parameters annotated with
`int` and `float`.  Of course, those functions only take
a single string argument; `my_converter()` takes two
annotated positional parameters.
But the principles remain the same.

Still, this is a more complex situation than we've seen before.
`recurse` takes a positional parameter `b` that has a default
value, but its converter takes multiple positional parameters,
and one of those *also* has a converter that takes multiple
positional parameters.  How does Appeal map this to the
command-line?

Appeal "flattens" the tree of converter functions into a linear
series of arguments and options.  In this case the usage string
would look like this:

    recurse a [i f s]

This tells you the `recurse` command takes either one or four command-line
arguments.  That optional group of three command-line arguments
has a special name in Appeal: it's an "argument group".
Technically, Appeal views this command-line as taking two
"argument groups": the first group is required, and consumes
one command-line argument; the second group is optional, and
consumes three command-line arguments.

(We actually saw our first "argument group" in the
second example in the
**Annotations And Introspection** section above, but
that time the argument group was an oparg.)

Now let's add an option and see what changes:

```Python
import appeal
app = appeal.Appeal()

def int_float(i: int, f: float):
    return (i, f)

def my_converter(i_f: int_float, s: str, *, verbose=False):
    return [i_f, s, verbose]

@app.command()
def recurse2(a:str, b:my_converter=[(0, 0), '', False]):
    print(f"recurse2 a={a!r} b={b!r}")

app.main()
```

Now the usage looks like this:

    recurse2 a [i [-v|--verbose] f s]

Notice the way Appeal renders it in the usage
string--the options aren't created until *after* the first
argument in the optional argument group.  This may seem
strange but that's how it works.  That's how it *has* to work.

Why?  From a high conceptual level, Appeal doesn't know that
you've "entered" the optional argument group until it
sees the user supply the first argument for that group.
So it doesn't create the options defined in that group
until after the first argument.

This high conceptual level corresponds exactly to how Appeal
calls your function.  Consider, if the user runs this command:

    recurse2 xyz

Appeal calls your function like so:

```Python
recurse2('xyz')
```

Since Appeal never called `my_converter()`, it can't
map `--verbose`.  It can only map `--verbose` once it
knows it's going to call `my_converter()`, and that
only becomes true the moment you supply that second
command-line argument.

Once