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.
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.
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.
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
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
Passing shell_command=False to the app constructor disables the shell command entirely.
Passing shell_clear_command=False to the app constructor disables the automatically-provided shell clear command. If shell_command is False, this has no effect (the clear command will not be added).
You can change the prompt by passing a string to shell_prompt in the app constructor. %name will be substituted with the application name. E.g. shell_prompt='%name%' would make downloader's shell prompt downloader%.
You can change the shell's alias by passing shell_alias to the app constructor. This accepts the same values as other aliases (string or sequence of strings). Defaults to sh.
You can indicate that a subcommand should be unavailable in the command shell by passing shell=False to the decorator. E.g.:
@downloader(shell=False) def no_shell_example(): print 'I will be available only from the command-line'
You can indicate a subcommand should be unavailable from the command line by passing cli=False to the decorator. E.g.:
@downloader(cli=False) def no_cli_example(): print 'I will be available only in the command shell'
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
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) # ...
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
You can change the ConfigParser class by passing a value to configparser_class in the app constructor. By default this is ConfigParser.SafeConfigParser.
conf_locations determines the base list of places to look for configuration. This can be a string or sequence of strings. %name is replaced with the application's name. Defaults to ('/etc/%name', '~/.%name').
Disable configuration via --config by passing conf_opts=False to the app constructor.
You can change the envvar name by passing a string to conf_envvar_name. %NAME will be substituted with the app name in all caps. For example, to change the envvar name from DOWNLOADER_CONFIG to DOWNLOADER_CFG:
downloader = clik.App('downloader', conf_envvar_name='%NAME_CFG')
Disable config via envvar by passing conf_envvar_name=None.
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
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:
log_enabled: If True, a file-based logging handler is created for the application and attached to the logger with the name given in log_name. Defaults to False.
log_name: Logger name. "%name" is substituted with the application name. Defaults to "%name".
log_filename: Default filepath for the application's log (the user can override the log path). "%name" is subsituted with the application name. Defaults to "~/%name.log".
log_level: Default logging level for the application (user user can override the log level). Defaults to logging.INFO.
log_format: The string to use for the logging.Formatter for the handler. Defaults to "%(asctime)s %(levelname)s %(message)s".
log_handler_class: File-based handler class to use. Defaults to logging.handlers.RotatingFileHandler.
log_handler_kwargs: Dictionary to use as keyword arguments when constructing log_handler_class. filename will always be added to this dictionary. Defaults to:
{'maxBytes': 10 * 1024 * 1024, 'backupCount': 10, 'delay': True}
log_conf: If True, the user can configure logging via config files (assuming conf_enabled=True). Defaults to True.
log_conf_section: The section of the configuration file containing log filepath and log level options. "%name" is substituted with the application name. Defaults to "%name".
log_conf_filename_option: Name of the option inside log_conf_section where the log filepath is specified. "%name" is substituted with the application name. Defaults to "log_filename".
log_conf_level_option: Name of the option inside log_conf_section where the log filepath is specified. "%name" is substituted with the application name. Defaults to "log_level".
log_opts: If True and logging is enabled, subcommands will accept --log-filename and --log-level options which can be used to configure logging on a per-call basis. Defaults to True (ie. "on" if logging is enabled).
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
name: Application name. Required.
description: One-line description of the application. Defaults to None.
version: Version number string. Defaults to None.
args_callback: Callback function for adding arguments for subcommands. Defaults to None.
conf_enabled: Boolean indicating whether the config system is enabled. Defaults to False.
conf_defaults: Dictionary of dictionaries specifying defaults for the config system. Can also be a string naming a module that has a conf_defaults attribute with the same format (dictionary of dictionaries of defaults). Defaults to {}.
conf_envvar_name: Environment variable name letting the user specify a path to a config file. If None, configuration via envvar is not allowed. Defaults to %NAME_CONFIG.
configparser_class: ConfigParser based class for the config object. Defaults to ConfigParser.SafeConfigParser.
log_enabled: If True, a file-based logging handler is created for the application and attached to the logger with the name given in log_name. Defaults to False.
log_name: Logger name. "%name" is substituted with the application name. Defaults to "%name".
log_filename: Default filepath for the application's log (the user can override the log path). "%name" is subsituted with the application name. Defaults to "~/%name.log".
log_level: Default logging level for the application (user user can override the log level). Defaults to logging.INFO.
log_format: The string to use for the logging.Formatter for the handler. Defaults to "%(asctime)s %(levelname)s %(message)s".
log_handler_class: File-based handler class to use. Defaults to logging.handlers.RotatingFileHandler.
log_handler_kwargs: Dictionary to use as keyword arguments when constructing log_handler_class. filename will always be added to this dictionary. Defaults to:
{'maxBytes': 10 * 1024 * 1024, 'backupCount': 10, 'delay': True}
log_conf: If True, the user can configure logging via config files (assuming conf_enabled=True). Defaults to True.
log_conf_section: The section of the configuration file containing log filepath and log level options. "%name" is substituted with the application name. Defaults to "%name".
log_conf_filename_option: Name of the option inside log_conf_section where the log filepath is specified. "%name" is substituted with the application name. Defaults to "log_filename".
log_conf_level_option: Name of the option inside log_conf_section where the log filepath is specified. "%name" is substituted with the application name. Defaults to "log_level".
log_opts: If True and logging is enabled, subcommands will accept --log-filename and --log-level options which can be used to configure logging on a per-call basis. Defaults to True (ie. "on" if logging is enabled).
opts: Application-wide options. Can be an optparse.Option or sequence of options. Defaults to None.
console_opts: Whether -v/-q/--no-color are enabled. Defaults to False.
conf_opts: If conf_enabled=True and this is True, the user can set the configuration file via the --config option. Defaults to True.
log_opts: If log_enabled=True and this is True, the user can configure the log filename and log level via --log-filename and --log-level options.
shell_command: If True, provide the shell command for this application. Defaults to True.
shell_alias: String or list of strings for the shell command's alias. Defaults to sh.
shell_prompt: Prompt for the shell. "%name" is substituted with the application name. Defaults to "(%name)> ".
shell_clear_command: If shell_command=True and this is True, add the clear command to the command shell.
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)
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! """ |