Clik

Clik (CLI Kit) requires Python 2.6 [1].

Clik provides glue code for subcommand-style CLI applications. It does command dispatch and option parsing. It provides a terminal output helper connected to the standard -v/-q flags. It transparently gives your application a command shell. If you want, clik will set up a ConfigParser instance and read in ini-style config files for you. If your application wants logging, clik can set up a logging handler for you as well.

Clik is one module and less than a thousand lines of code, so it's easy to read, modify and include in your projects.

This README is an introduction, tutorial and the documentation wrapped in one. It shows the development of a simple app named downloader, which downloads content to a local directory. You can check out the example code, which basically matches the development of the code in this file, from git://github.com/jds/downloader. The output is included in the repository files; if you're not interested in my commentary you can simply step through the history.

Installation

Setuptools:

easy_install clik

Or download the latest version from http://pypi.python.org/pypi/clik, extract the tarball and run python setup.py install.

The git repository is at git://github.com/jds/clik.

The Basics

To start writing an application, you'll create an instance of clik.App and call its main method:

import clik

downloader = clik.App('downloader')

if __name__ == '__main__':
    downloader.main()

At this point you have a working application:

$ python downloader.py
downloader
Basic usage: downloader <subcommand> [options]

shell, sh
    A command shell for this application.

Run downloader <command> -h for command help

Typically you'll also want to provide a version and short summary:

downloader = clik.App('downloader',
                      version='1.0',
                      description='Manages downloads in a local directory.')


$ python downloader.py
downloader 1.0 -- Manages downloads in a local directory.
Basic usage: downloader <subcommand> [options]
# same output as before

$ python downloader.py --version
1.0

Add subcommands by defining a function and decorating it with the app instance. The command name will be the function name:

@downloader
def hello_world():
    print 'Hello, world!'


$ python downloader.py
downloader 1.0 -- Manages downloads in a local directory.
Basic usage: downloader <subcommand> [options]

hello_world
    No description.

shell, sh
    A command shell for this application.

Run downloader <command> -h for command help


$ python downloader.py hello_world -h
Usage: downloader hello_world [options]

No description.

Options:
  --version   show program's version number and exit
  -h, --help  show this help message and exit


$ python downloader.py hello_world
Hello, world!

Help is taken from the function's docstring, if it exists. The docstring should be formatted conventionally [2]:

  @downloader
  def hello_world():
      """
      Says hello to the world.

      For nontrivial commands, this text right here would be a
      more thorough description of what the command does and how
      to use it. For hello_world, you'd typically just use a
      one-liner with no extended help.
      """

$ python downloader.py
downloader 1.0 -- Manages downloads in a local directory.
Basic usage: downloader <subcommand> [options]

hello_world
    Says hello to the world.

shell, sh
    A command shell for this application.

Run downloader <command> -h for command help


$ python downloader.py hello_world -h
Usage: downloader hello_world [options]

Says hello to the world.

Options:
  --version   show program's version number and exit
  -h, --help  show this help message and exit

For nontrivial commands, this text right here would be a
more thorough description of what the command does and how
to use it. For hello_world, you'd typically just use a
one-liner with no extended help.

hello_world is aptly named but a bit painful to type over and over again. Adding shorter names is easy:

@downloader(alias='hw')
def hello_world():
    print 'Hello, world!'

# or

@downloader(alias=['hw', 'hllwrld'])
def hello_world():
    print 'Hello, world!'


$ python downloader.py
downloader 1.0 -- Manages downloads in a local directory.
Basic usage: downloader <subcommand> [options]

hello_world, hw, hllwrld
    Says hello to the world.

shell, sh
    A command shell for this application.

Run downloader <command> -h for command help


$ python downloader.py hw
Hello, world!

$ python downloader.py hllwrld
Hello, world!

Of course, clik makes sure your names don't run over each other:

@downloader
def hw():
    print 'You will not see me because the script will not run!'


$ python downloader.py
Traceback (most recent call last):
  File "downloader.py", line 22, in <module>
    @downloader
  File "/Users/jds/.virtualenvs/clik-tutorial/lib/python2.6/site-packages/clik.py", line 55, in __call__
    self.add(maybe_fn)
  File "/Users/jds/.virtualenvs/clik-tutorial/lib/python2.6/site-packages/clik.py", line 199, in add
    existing_fn.__module__, existing_fn.__name__))
ValueError: Command name hw from __main__.hw conflicts with name defined in __main__.hello_world

This is typical of much of the interaction with clik: start with the least amount of code necessary, extend application-wide functionality by providing arguments to the app constructor and configure subcommand-level functionality by providing arguments to the decorator.

Before moving on, I should be clear: while hello_world() has started its life as a typical function, its signature is inspected at runtime and dynamically passed the desired arguments. You may only ask for arguments with known names. Base arguments:

You can extend (or override, if you want) the argument values by providing the args_callback a value in the app constructor:

def my_callback(opts, args):
    # my_callback can take any of the base objects as arguments.
    return {'conf': MyConfigObject(), 'someval': AnotherThing()}

downloader = clik.App('downloader',
                      args_callback=my_callback)

@downloader
def my_subcommand(conf, someval):
    # conf will be the MyConfigObject()
    # someval is the AnotherThing instance

downloader eventually makes use of all these facilities. Read on to see how.

Downloader

Let's get down to business with the code. I'll start by showing you the basic working implementation and then trick it out with only a bit more code.

downloader should be able to list files in a local downloads directory, remove files from the directory and download data from URLs into the directory.

import os
import urllib
import urlparse

import clik


DOWNLOADS_PATH = os.path.join(os.path.dirname(__file__), 'downloads')


def downloads_dir():
    path = os.path.expanduser(os.path.expandvars(DOWNLOADS_PATH))
    if not os.path.exists(path):
        os.mkdir(path)
    return path


downloader = clik.App('downloader',
                      version='1.0',
                      description='Manages downloads in a local directory.')


@downloader(alias='ls')
def list():
    """List the contents of the downloads directory."""
    downloads = downloads_dir()
    filenames = os.listdir(downloads)
    for filename in filenames:
        print filename
    return 0


@downloader(alias='rm')
def remove(args):
    """Remove a downloaded file."""
    if len(args) < 1:
        print >>sys.stderr, 'error: expecting at least one filename to remove'
        return 1
    downloads = downloads_dir()
    for arg in args:
        path = os.path.join(downloads, arg)
        if os.path.exists(path):
            os.unlink(path)
        else:
            print >>sys.stdout, 'no such file or directory: '+path
    return 0


@downloader(alias='dl')
def download(args):
    if len(args) < 1:
        print >>sys.stderr, 'error: you must provide a URL'
        return 1
    url = args[0]

    if len(args) > 1:
        name = args[1]
    else:
        name = urlparse.urlparse(url).path.split('/')[-1]
        if not name:
            name = 'index.html'

    downloads = downloads_dir()
    download_path = os.path.join(downloads, name)
    if os.path.exists(download_path):
        return 0

    print 'fetching %s...' % url
    try:
        urllib.urlretrieve(url, download_path)
    except IOError, e:
        print >>sys.stderr, e
        return 1
    return 0

In action:

$ python downloader.py dl -h
Usage: downloader download|dl [options]

No description.

Options:
  --version   show program's version number and exit
  -h, --help  show this help message and exit


$ python downloader.py ls -h
Usage: downloader list|ls [options]

List the contents of the downloads directory.

Options:
  --version   show program's version number and exit
  -h, --help  show this help message and exit


$ python downloader.py rm -h
Usage: downloader remove|rm [options]

Remove a downloaded file.

Options:
  --version   show program's version number and exit
  -h, --help  show this help message and exit


$ python downloader.py ls
$ python downloader.py dl http://python.org python-index.html
fetching http://python.org...
$ python downloader.py dl http://python.org python-index.html
$ python downloader.py ls
python-index.html
$ python downloader.py rm python-index.html
$ python downloader.py ls
$

The first niggling issue to take care of is the usage for dl and rm. Clik will always make the usage start with <app-name> <command-name> but you can override what comes after:

@downloader(alias='rm', usage='[file1 [file2 [...]]] [options]')
def remove(args):
    ...

@downloader(alias='dl', usage='URL [local-name] [options]')
def download(args):
    ...


$ python downloader.py dl -h
Usage: downloader download|dl URL [local-name] [options]

No description.

Options:
  --version   show program's version number and exit
  -h, --help  show this help message and exit


$ python downloader.py rm -h
Usage: downloader remove|rm [file1 [file2 [...]]] [options]

Remove a downloaded file.

Options:
  --version   show program's version number and exit
  -h, --help  show this help message and exit

Command Shell

Perhaps you noticed the shell/sh command I've neglected to talk about thus far. In my opinion, clik's best feature is the transparently-provided command shell for your application. Without changing a single line, downloader can do this:

$ python downloader.py sh
(downloader)> ?

Documented commands (type help <topic>):
========================================
clear  download  exit  list  quit  remove

Undocumented commands:
======================
dl  help  ls  rm

(downloader)> ? download
Usage: download|dl URL [local-name] [options]

No description.

Options:
  --version   show program's version number and exit
  -h, --help  show this help message and exit


(downloader)> ls
(downloader)> dl http://python.org python-index.html
fetching http://python.org...
(downloader)> dl http://python.org python-index.html
(downloader)> ls
python-index.html
(downloader)> rm python-index.html
(downloader)> ls
(downloader)> exit
$

Aliases are listed as undocumented commands so that "working command set" is clear.

Nitty Gritty

Options

Right now, the downloads path is hardcoded into downloader. Let's add an option to the app to let users specify which directory should contain the downloads:

from optparse import make_option as opt

def downloads_dir(opts):
    path = opts.downloads_directory or DOWNLOADS_PATH
    # ...

downloader = clik.App('downloader',
                      opts=opt('-d', '--downloads-dir',
                               dest='downloads_directory', default=None,
                               help=('Directory where downloads are stored '
                                     '[default: '+DOWNLOADS_PATH+']')))


# Add ``opts`` to each subcommand signature and call to downloads_dir() E.g.

@downloader(alias='ls')
def list(opts):
    downloads = downloads_dir(opts)
    # ...


$ python downloader.py ls -h
Usage: downloader list|ls [options]

List the contents of the downloads directory.

Options:
  -d DOWNLOADS_DIRECTORY, --downloads-dir=DOWNLOADS_DIRECTORY
                        Directory where downloads are stored [default:
                        downloads]
  --version             show program's version number and exit
  -h, --help            show this help message and exit


$ python downloader.py ls
$ python downloader.py dl http://python.org
fetching http://python.org...
$ python downloader.py ls
index.html
$ python downloader.py ls -d otherdir
$ python downloader.py dl http://python.org -d otherdir
fetching http://python.org...
$ python downloader.py ls -d otherdir
index.html
$ python downloader.py rm index.html -d otherdir
$ python downloader.py ls -d otherdir
$ python downloader.py ls
index.html

This is a step in the right direction, but still pretty annoying as there's no way to permanently specify the downloads directory. We'll deal with this later on, with the configuration system.

Note that opts can be a single optparse.Option or a sequence of options.

If you've tried to download content from a nonexistent URL, you might have noticed that downloader hangs forever (or, longer than I was willing to wait to find out). We'll add a -t option to let users specify the timeout.

Also, to get a fresh copy of a URL, the user must rm the local file before running dl. We'll add an -o option so users can indicate they'd like a fresh download:

@downloader(alias='dl', usage='URL [local-name] [options]',
            opts=(opt('-t', '--timeout', dest='timeout', type='int',
                      default=30, help='Connection timeout [default %default]'),
                  opt('-o', '--overwrite', dest='overwrite', action='store_true',
                      default=False, help='Overwrite (re-download) file')))
def download(args, opts):
    # ...
    if os.path.exists(download_path):
        if opts.overwrite:
            os.unlink(download_path)
        else:
            return 0

    import socket
    socket.setdefaulttimeout(opts.timeout)
    print 'fetching %s...' % url
    # ...



$ python downloader.py dl -h
Usage: downloader download|dl URL [local-name] [options]

Downloads content from the internet.

Options:
  -t TIMEOUT, --timeout=TIMEOUT
                        Connection timeout [default 30]
  -o, --overwrite       Overwrite (re-download) file
  -d DOWNLOADS_DIRECTORY, --downloads-dir=DOWNLOADS_DIRECTORY
                        Directory where downloads are stored [default:
                        downloads]
  --version             show program's version number and exit
  -h, --help            show this help message and exit


$ python downloader.py ls
$ python downloader.py dl http://python.org
fetching http://python.org...
$ python downloader.py dl http://python.org
$ python downloader.py dl http://python.org -o
fetching http://python.org...
$ python downloader.py ls
index.html

Supplying Custom Arguments

Each subcommand has to call downloads_dir, which is annoying (especially in the case of ls and rm which otherwise don't use the options argument). In this case you can use the args_callback argument to the app constructor. args_callback should be a function whose signature follows the same rules as subcommands and should return a dictionary with {argument name: value} pairs.

def downloads_dir(opts):
    # ...
    return {'downloads': path}

downloader = clik.App('downloader',
                      args_callback=downloads_dir)

def list(downloads):
    # -downloads = downloads_dir(opts)
    # ...

def remove(args, downloads):
    # -downloads = downloads_dir(opts)
    # ...

def download(args, opts, downloads):
    # -downloads = downloads_dir(opts)
    # ...

Configuration

Sooner or later every CLI application needs configuration. Clik provides for ini-style configuration with the common pattern of reading a list of files, each successive file's configuration overriding any previously-read values. By default, clik will look for configuration files in /etc/your-app-name, then ~/.your-app-name, then the filepath given in the $YOURAPPNAME_CONFIG envvar, then the value of --config, if provided.

For downloader, we want the user to be able to permanently configure the download directory. In their configuration file, they'll set it up like this:

[downloader]
path = /path/to/their/downloads

To enable configuration, add conf_enabled=True to the app constructor and specify the defaults:

downloader = clik.App('downloader',
                      conf_enabled=True,
                      conf_defaults={'downloader': {'path': DOWNLOADS_PATH}})

conf_defaults should be a dictionary of dictionaries representing the default sections and options. It can also be a string pointing to a module with a similarly-defined conf_defaults attribute. That is, we could create a file "downloader_conf.py", define conf_defaults in that file, and use clik.App(conf_defaults='downloader_conf').

When conf is enabled, subcommands can take the conf argument, which will be an instance of ConfigParser.SafeConfigParser by default. Because the directory-handling code for downloader is in downloads_dir() we'll add the config-handling code there:

def downloads_dir(opts, conf):
    path = opts.downloads_directory or conf.get('downloader', 'path') or DOWNLOADS_PATH
    # ...


$ python downloader.py ls -h
Usage: downloader list|ls [options]

List the contents of the downloads directory.

Options:
  -d DOWNLOADS_DIRECTORY, --downloads-dir=DOWNLOADS_DIRECTORY
                        Directory where downloads are stored [default:
                        downloads]
  --config=CONF_PATH    Path to config file (will read /etc/downloader,
                        ~/.downloader, $DOWNLOADER_CONFIG, then this value, if
                        set)
  --version             show program's version number and exit
  -h, --help            show this help message and exit


$ python downloader.py ls
$ python downloader.py dl http://python.org
fetching http://python.org...
$ python downloader.py ls
index.html
$ cat >>~/.downloader
[downloader]
path = ./downloads2
^C
$ python downloader.py ls
$ python downloader.py dl http://python.org
fetching http://python.org...
$ python downloader.py ls
index.html
$ ls
README              downloader.py   downloads       downloads2
$ cat >>cfg
[downloader]
path = ./downloads3
^C
$ export DOWNLOADER_CONFIG=./cfg
$ python downloader.py ls
$ python downloader.py dl http://python.org
fetching http://python.org...
$ python downloader.py ls
index.html
$ ls
README      downloader.py   downloads2
cfg         downloads       downloads3
$ cat >>cfg2
[downloader]
path = ./downloads4
^C
$ python downloader.py ls --config=./cfg2
$ python downloader.py dl http://python.org --config=./cfg2
fetching http://python.org...
$ python downloader.py ls --config=./cfg2
index.html
$ ls
README      cfg2            downloads       downloads3
cfg         downloader.py   downloads2      downloads4

Details

Console Output

Another common need among CLI applications is output control (-v/-q options). To enable those options, add console_opts=True to the app constructor:

downloader = clik.App('downloader',
                      console_opts=True)

Subcommand functions can take the console object, which has these methods:

console.quiet('Always emitted to stdout')
console.q('Alias for console.quiet()')
console.normal('Emitted if the user does not pass -q')
console.n('Alias for console.normal()')
console.verbose('Emitted only if the user passes -v')
console.v('Alias for console.verbose()')
console.error('Always emitted to stderr')

By default, a single newline is emitted after the string. You can change that using the newlines argument:

console.n('Doing something...', newlines=0)
console.n('done')

There is also a small colorization markup language:

console.q('<red>Error:</> something bad happened.')

The complete list of colors is in the appendix.

Updating downloader to use the console system:

def downloads_dir(opts, conf, console):
    path = opts.downloads_directory or conf.get('downloader', 'path') or DOWNLOADS_PATH
    path = os.path.expanduser(os.path.expandvars(path))
    console.v('downloads directory is '+path)
    if not os.path.exists(path):
        console.v('downloads directory does not exist, creating')
        os.mkdir(path)
    return {'downloads': path}


downloader = clik.App('downloader',
                      version='1.0',
                      description='Manages downloads in a local directory.',
                      console_opts=True,
                      conf_enabled=True,
                      conf_defaults={'downloader': {'path': DOWNLOADS_PATH}},
                      opts=opt('-d', '--downloads-dir',
                               dest='downloads_directory', default=None,
                               help=('Directory where downloads are stored '
                                     '[default: '+DOWNLOADS_PATH+']')),
                      args_callback=downloads_dir)


@downloader(alias='ls')
def list(downloads, console):
    """List the contents of the downloads directory."""
    filenames = os.listdir(downloads)
    console.n('%i files in downloads' % len(filenames))
    for filename in filenames:
        console.q(filename)
    return 0


@downloader(alias='rm', usage='[file1 [file2 [...]]] [options]')
def remove(args, downloads, console):
    """Remove a downloaded file."""
    if len(args) < 1:
        console.error('error: expecting at least one filename to remove')
        return 1
    for arg in args:
        path = os.path.join(downloads, arg)
        if os.path.exists(path):
            console.v('removing '+path)
            os.unlink(path)
        else:
            console.error('<red>error:</> no such file or directory: '+path)
    return 0


@downloader(alias='dl', usage='URL [local-name] [options]',
            opts=(opt('-t', '--timeout', dest='timeout', type='int',
                      default=30, help='Connection timeout [default %default]'),
                  opt('-o', '--overwrite', dest='overwrite', action='store_true',
                      default=False, help='Overwrite (re-download) file')))
def download(args, opts, downloads, console):
    """Downloads content from the internet."""
    if len(args) < 1:
        console.error('<red>error:</> you must provide a URL')
        return 1
    url = args[0]

    if len(args) > 1:
        name = args[1]
    else:
        name = urlparse.urlparse(url).path.split('/')[-1]
        if not name:
            name = 'index.html'
    console.v('url is %s, local name is %s' % (url, name))

    download_path = os.path.join(downloads, name)
    if os.path.exists(download_path):
        if opts.overwrite:
            console.v('local file already exists, overwriting')
            os.unlink(download_path)
        else:
            console.v('local file already exists, not downloading')
            return 0

    import socket
    socket.setdefaulttimeout(opts.timeout)
    console.n('fetching %s...' % url, newlines=0)
    try:
        urllib.urlretrieve(url, download_path)
    except IOError, e:
        console.n('<red>error</>')
        console.error(e)
        return 1
    console.n('<bold>done</>')
    return 0

In the terminal:

$ python downloader.py dl -h
Usage: downloader download|dl URL [local-name] [options]

Downloads content from the internet.

Options:
  -t TIMEOUT, --timeout=TIMEOUT
                        Connection timeout [default 30]
  -o, --overwrite       Overwrite (re-download) file
  -d DOWNLOADS_DIRECTORY, --downloads-dir=DOWNLOADS_DIRECTORY
                        Directory where downloads are stored [default:
                        downloads]
  -v, --verbose         Emit verbose information
  -q, --quiet           Emit only errors
  --no-color            Do not colorize output
  --config=CONF_PATH    Path to config file (will read /etc/downloader,
                        ~/.downloader, $DOWNLOADER_CONFIG, then this value, if
                        set)
  --version             show program's version number and exit
  -h, --help            show this help message and exit


$ python downloader.py dl http://python.org -v
downloads directory is downloads
downloads directory does not exist, creating
url is http://python.org, local name is index.html
fetching http://python.org...done

$ python downloader.py dl http://python.org -v
downloads directory is downloads
url is http://python.org, local name is index.html
local file already exists, not downloading

$ python downloader.py dl http://python.org -vo
downloads directory is downloads
url is http://python.org, local name is index.html
local file already exists, overwriting
fetching http://python.org...done

$ python downloader.py dl http://python.org

$ python downloader.py dl http://python.org -oq

$ python downloader.py ls
1 files in downloads
index.html

$ python downloader.py ls -q
index.html

$ python downloader.py ls -v
downloads directory is downloads
1 files in downloads
index.html

$ python downloader.py rm foo
error: no such file or directory: downloads/foo

$ python downloader.py rm foo --no-color
error: no such file or directory: downloads/foo

$ python downloader.py rm index.html -v
downloads directory is downloads
removing downloads/index.html

Logging

Last but not least, clik provides an easy, flexible way to set up file-based logging. To get started, set log_enabled=True in the app constructor:

downloader = clik.App('downloader',
                      log_enabled=True)

Subcommands can take the log argument, which will be the logging.Logger instance for the application:

def downloads_dir(opts, conf, console, log):
    # ...
    if not os.path.exists(path):
        msg = 'downloads directory does not exist, creating'
        log.info(msg)
        console.v(msg)
        os.mkdir(path)
    return {'downloads': path}

def remove(args, downloads, console, log):
    # ...
    for arg in args:
        path = os.path.join(downloads, arg)
        if os.path.exists(path):
            console.v('removing '+path)
            os.unlink(path)
            log.info('removed '+path)
        else:
            console.error('<red>error:</> no such file or directory: '+path)
    return 0

def download(args, opts, downloads, console, log):
    # ...
    download_path = os.path.join(downloads, name)
    if os.path.exists(download_path):
        if opts.overwrite:
            console.v('local file already exists, overwriting')
            os.unlink(download_path)
            log.info('removed '+download_path)
        else:
            console.v('local file already exists, not downloading')
            return 0

    import socket
    socket.setdefaulttimeout(opts.timeout)
    console.n('fetching %s...' % url, newlines=0)
    try:
        urllib.urlretrieve(url, download_path)
    except IOError, e:
        console.n('<red>error</>')
        console.error(e)
        log.error('could not fetch %s: %s' % (url, e))
        return 1
    log.info('fetched '+url)
    console.n('<bold>done</>')
    return 0

In the shell:

$ python downloader.py dl -h
Usage: downloader download|dl URL [local-name] [options]

Downloads content from the internet.

Options:
  -t TIMEOUT, --timeout=TIMEOUT
                        Connection timeout [default 30]
  -o, --overwrite       Overwrite (re-download) file
  -d DOWNLOADS_DIRECTORY, --downloads-dir=DOWNLOADS_DIRECTORY
                        Directory where downloads are stored [default:
                        downloads]
  -v, --verbose         Emit verbose information
  -q, --quiet           Emit only errors
  --no-color            Do not colorize output
  --config=CONF_PATH    Path to config file (will read /etc/downloader,
                        ~/.downloader, $DOWNLOADER_CONFIG, then this value, if
                        set)
  --log-filename=LOG_FILENAME
                        Log to file [default: ~/downloader.log]
  --log-level=LOG_LEVEL
                        Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
                        [default INFO]
  --version             show program's version number and exit
  -h, --help            show this help message and exit


$ python downloader.py dl http://python.org
fetching http://python.org...done
$ python downloader.py rm index.html
$ python downloader.py dl http://does.not.exist
fetching http://does.not.exist...error
[Errno socket error] [Errno 8] nodename nor servname provided, or not known
$ cat ~/downloader.log
2010-01-02 05:46:59,274 INFO downloads directory does not exist, creating
2010-01-02 05:46:59,886 INFO fetched http://python.org
2010-01-02 05:47:02,409 INFO removed downloads/index.html
2010-01-02 05:47:05,226 ERROR could not fetch http://does.not.exist: [Errno socket error] [Errno 8] nodename nor servname provided, or not known
$

Detail

Logging is extremely configurable, both by you and your end users. Arguments to the app constructor that affect logging:

Configuration via config files. For example, you can change the log filepath and level for downloader by setting log_filepath and log_level in one of the configuration files:

[downloader]
path = path/to/the/downloads/dir
log_filepath = ~/my-logs/downloader.log
log_level = DEBUG

Appendix: clik.App Constructor Arguments

Appendix: Decorator Arguments

You can also selectively disable any of the automatically added options. Note that this generally is not a good idea as that option becomes "global-except-for-that-one-command", which is annoying. Pass False to any one of these arguments to disable the associated options:

In the extreme case where you want to turn off all these options, you can pass global_opts=False. With global_opts=False, you can selectively add the options back in by setting the associated arguments to True. For example, to disable all arguments except -h/--help, the decorator would be:

 @downloader(console_opts=False, version_opts=False, conf_opts=False,
             log_opts=False, app_opts=False)

# or

@downloader(global_opts=False, help_opts=True)

Appendix: Terminal Colors

These are the colors in the clik.Console library:

clik.Console is based on Georg Brandl's Sphinx project's console.py.

[1]2.5 compatibility is the first item on the TODO list after this README. Nitty-gritty: The only 2.6 feature clik uses is the delay argument to logging.RotatingFileHandler. This can either be simulated by a wrapper or simply omitted (the user can configure the logging handler and arguments, anyway). As for <2.5, I don't know.
[2]

Specifically, clik can properly handle docstrings that consist of one line:

def hello_world():
    """Says hello to the world."""

def hello_world():
    """
    Says hello to the world.
    """

Docstrings with more information should have a one line description followed by a blank line followed by the extended info:

def hello_world():
    """
    Says hello to the world.

    If there were more to say about a hello world function,
    this is where it would go. The indentation of the first
    line after the short description is used as the
    baseline for the rest of the text. Otherwise,
    formatting is preserved. This is unlike optparse's
    handling of `epilog`, which annoyingly reformats the
    input its given. That makes it hard to write
    clearly-formatted examples, which is exactly
    what you want to do in the "more help" text!
    """