.. _cli:

================================
CLI: Makes CLI easier for Python
================================

:author: Will Maier <will@m.aier.us>

Overview
========

The CLI App classes (hereafter 'App') try to make writing command
line applications in Python less boring by handling the obvious and
easy bits. Let App parse options and arguments, read configuration
files, interpret the environment and build a logger for you!

App makes writing easy to use scripts as simple as defining a
function::

    def helloworld(app):
        print 'hello world.'

    if __name__ == '__main__':
        from cli import App

        app = App(helloworld)
        app.run()

If you save that in 'helloworld.py' and run it, you should get the
following::

    $ python ./helloworld.py
    hello world.

But there's more! App automatically created an option parser (using
Python's standard optparse package) for you::

    $ python ./helloworld.py -h
    Usage: helloworld 

    Options:
      -h, --help  show this help message and exit

As we proceed, you'll find out that App did a few more things behind
the scenes for you, too, but it's worth investigating the option
parsing in greater depth. But first, a quick explanation of the App
interface.

App Interface
=============

All App wants from you is a function (or other callable). Your
function must accept at least one argument (the App object itself).
The name of your function (stored in the .func_name attribute) or
callable class will become the name of the application. Similarly,
the docstring for your function or callable (hereafter simply
referred to as the function) will become your application's usage
statement.

App may also pass arguments and options to your function, so it
should be prepared to handle arguments and keyword arguments, too.
In most cases, the signature for an App function will look something
like this::

    def yourapp(app, *args, **kwargs):
        pass

Option Parsing
==============

App parses options and arguments on the command line and passes them
to your application as arguments. By default, the only valid option
is '-h', which will print your application's usage message (ie its
docstring). But it's easy to add your own::

    def helloworldlang(app, *args, **kwargs):
        """[options]

        Print 'hello world' in the requested language.
        """
        msg = 'hello world'
        if kwargs['esperanto']:
            msg = 'Saluton mondo'

        print('%s!' % msg)

    if __name__ == '__main__':
        from cli import App
        app = App(helloworldlang)
        app.add_option("esperanto",
            default=False,
            help="print message in Esperanto",
            action="store_true")
        app.run()

Save that as 'helloworldlang.py' and run it::

    $ python ./helloworldlang.py -h
    Usage: helloworldlang 

        Print 'hello world' in the requested language.
        

    Options:
      -h, --help       show this help message and exit
      -e, --esperanto  print message in Esperanto
    $ python ./helloworldlang.py
    hello world!
    $ python ./helloworldlang.py -e
    Saluton mondo!

Presto!

You can also create options that expect arguments of their own. For
example, adding a new option for each new language helloworldlang.py
supports will quickly become annoying. Instead, it would be simpler
to create a single language option to which we can assign arbitrary
languages. This is easy to do using the 'store' action (which is
also the default for add_option())::

    def helloworldlang(app, *args, **kwargs):
        """[-l language]

        Print 'hello world' in the requested language.
        """
        msg = 'hello world'
        if kwargs['language'] == 'esperanto':
            msg = 'Saluton mondo'

        print('%s!' % msg)

    if __name__ == '__main__':
        from cli import App
        app = App(helloworldlang)
        app.add_option("language",
            default='english',
            help="print message in LANGUAGE",
            action="store")
        app.run()

Now, we get the following when we run helloworldlang.py::

    $ python ./helloworldlang.py -h
    Usage: helloworldlang [-l language]

        Print 'hello world' in the requested language.
        

    Options:
      -h, --help            show this help message and exit
      -l LANGUAGE, --language=LANGUAGE
                            print message in Esperanto
    $ python ./helloworldlang.py
    hello world!
    $ python ./helloworldlang.py -l esperanto
    Salutan mondo!

You can add lots of other kinds of options using the add_option()
method; for more information, see the optparse documentation. For
example, you could create a 'verbose' option that increments a
verbosity counter and then decide whether or not to print log or
debugging messages based on that counter.

Since that's a common scenario, though, App makes it easy...

Logging
=======

LoggingApp provides a logger (from logging.Logger) and handy options
for controlling its verbosity::

    def helloworldlang(app, *args, **kwargs):
        """[options] [-l language]

        Print 'hello world' in the requested language.
        """
        msg = 'hello world'
        if kwargs['language'] == 'esperanto':
            app.log.debug("switch to esperanto")
            msg = 'Saluton mondo'

        print('%s!' % msg)

    if __name__ == '__main__':
        from cli import LoggingApp
        app = LoggingApp(helloworldlang)
        app.add_option("language",
            default='english',
            help="print message in Esperanto",
            action="store")
        app.run()

If you run the above, you'll find that your application just grew a
few more options:

    $ python ./helloworldlog.py -h
    Usage: helloworldlang [-l language]

        Print 'hello world' in the requested language.
        

    Options:
      -h, --help            show this help message and exit
      -v, --verbose         raise the verbosity
      -q, --quiet           decrease the verbosity
      -s, --silent          only log warnings
      -l LANGUAGE, --language=LANGUAGE
                            print message in Esperanto

The 'verbose' and 'quiet' options increase and decrease,
respectively, the log level for the App's log instance. Without any
arguments, warnings, errors and critical messages will be printed.
With one '-v', the logger will also print informational messages;
with two, you'll see everything. Here it is in action:

    $ python ./helloworldlog.py -l esperanto
    Saluton mondo!
    $ python ./helloworldlog.py -l esperanto -vv
    switch to esperanto
    Saluton mondo!

You can configure the Logger's message and date formats by adjusting
the LoggingApp's 'message_format' and 'date_format' attributes (or
passing them in as keyword arguments when you instantiate the
LoggingApp).

Other Configuration Methods
===========================

More complicated applications may check environment variables or
even read configuration files. With App, both of these sources are
boiled down into plain old arguments and keyword arguments for your
application function. App will read your configuration file and
check the environment before parsing the command line. If, after
resolving the configuration and environment options, it finds
multiple definitions for a given option, the most recent definition
wins. Just remember that CLI beats environment beats config file.

App uses Python's standard ConfigParser, so you can use the familiar
.ini style when writing configuration files. Simply tell App where
your configuration file lives::

    app = App(foo, config_file='sample.conf')

When reading the configuration file, App will flatten it, using '.'
to delimit levels present in the original file. This means that the
two sections in the following config snippet are identical::

    [section]
    foo.bar = quux

    [section.foo]
    bar = quux

Both of the above sections represent an option that your application
would access as follows:

    >>> kwargs['section']['foo']['bar']

See examples/helloworldconfig.py and examples/helloworld.conf for
more information.

App automatically looks for environment variables that start with
your application's name followed by an '_'. It then transforms the
variable name, dropping the application name prefix and lowercasing
the rest. It also replaces any other '_' characters with '.', which
allows nested options as described above for configuration files.

Note that App can't provide defaults for options that aren't defined
using add_option(). This means that you need to check before using
configuration and environment options:

    def helloworldenv(app, *args, **kwargs):
        if 'envvar' in kwargs:
            app.log.debug("Found envvar")

Installation
============

It's as easy as ``python setup.py install``. See Python's distutils
documentation for more options (or, alternatively, just copy cli.py
to whereever you need it).

License
=======

Copyright (c) 2008 Will Maier <will@m.aier.us>

Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

