rename from finaloption to opster
authorAlexander Solovyov <piranha@piranha.org.ua>
Wed, 19 Aug 2009 12:27:43 +0300
changeset 75 2782b2406ba8
parent 74 ecf86f596b72
child 76 d4bdbbf7a500
rename from finaloption to opster
README
docs/Makefile
docs/api.rst
docs/conf.py
docs/index.rst
docs/make.bat
docs/overview.rst
finaloption.py
opster.py
setup.py
test.py
test_cmd.py
test_opts.py
--- a/README	Fri Aug 07 19:23:13 2009 +0300
+++ b/README	Wed Aug 19 12:27:43 2009 +0300
@@ -1,24 +1,22 @@
 .. -*- mode: rst -*-
 
-=============
- Finaloption
-=============
+========
+ Opster
+========
 
-Finaloption is a command line parser, intended to make writing command line
+Opster is a command line parser, intended to make writing command line
 applications easy and painless. It uses built-in Python types (lists,
 dictionaries, etc) to define options, which makes configuration clear and
 concise. Additionally it contains possibility to handle subcommands (i.e.
 ``hg commit`` or ``svn update``).
 
-JFYI: name is derived from Die Krupps' song Final Option.
-
 Quick example
 -------------
 
 That's an example of an option definition::
 
   import sys
-  from finaloption import command
+  from opster import command
 
   @command(usage='%name [-n] MESSAGE')
   def main(message,
@@ -48,4 +46,4 @@
 line. This is also true for subcommands: read about that and everything else
 you'd like to know in `documentation`_.
 
-.. _documentation: http://hg.piranha.org.ua/finaloption/docs/
+.. _documentation: http://hg.piranha.org.ua/opster/docs/
--- a/docs/Makefile	Fri Aug 07 19:23:13 2009 +0300
+++ b/docs/Makefile	Wed Aug 19 12:27:43 2009 +0300
@@ -63,9 +63,9 @@
 	@echo
 	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
 	      ".qhcp project file in _build/qthelp, like this:"
-	@echo "# qcollectiongenerator _build/qthelp/Finaloption.qhcp"
+	@echo "# qcollectiongenerator _build/qthelp/Opster.qhcp"
 	@echo "To view the help file:"
-	@echo "# assistant -collectionFile _build/qthelp/Finaloption.qhc"
+	@echo "# assistant -collectionFile _build/qthelp/Opster.qhc"
 
 latex:
 	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex
--- a/docs/api.rst	Fri Aug 07 19:23:13 2009 +0300
+++ b/docs/api.rst	Wed Aug 19 12:27:43 2009 +0300
@@ -1,8 +1,8 @@
-=================
- Finaloption API
-=================
+============
+ Opster API
+============
 
-.. module:: finaloption
+.. module:: opster
 
 .. _api-command:
 .. autofunction:: command
--- a/docs/conf.py	Fri Aug 07 19:23:13 2009 +0300
+++ b/docs/conf.py	Wed Aug 19 12:27:43 2009 +0300
@@ -2,7 +2,7 @@
 
 import sys, os
 sys.path.append('..')
-import finaloption
+import opster
 
 # -- General configuration -----------------------------------------------------
 
@@ -10,9 +10,9 @@
 templates_path = ['_templates']
 source_suffix = '.rst'
 master_doc = 'index'
-project = u'Finaloption'
+project = u'Opster'
 copyright = u'2009, Alexander Solovyov'
-version = release = finaloption.__version__
+version = release = opster.__version__
 exclude_trees = ['_build']
 pygments_style = 'sphinx'
 
@@ -29,12 +29,12 @@
 #html_favicon = None
 html_static_path = ['_static']
 html_use_smartypants = True
-htmlhelp_basename = 'Finaloptiondoc'
+htmlhelp_basename = 'Opsterdoc'
 
 
 # -- Options for LaTeX output --------------------------------------------------
 
 latex_documents = [
-  ('index', 'Finaloption.tex', u'Finaloption Documentation',
+  ('index', 'Opster.tex', u'Opster Documentation',
    u'Alexander Solovyov', 'manual'),
 ]
--- a/docs/index.rst	Fri Aug 07 19:23:13 2009 +0300
+++ b/docs/index.rst	Wed Aug 19 12:27:43 2009 +0300
@@ -1,12 +1,6 @@
-=============
- Finaloption
 =============
-
-::
-
-  If that's the Final Option,
-  I'm gonna choose it.
-                   Die Krupps
+ Opster
+=============
 
 .. toctree::
    :maxdepth: 2
@@ -15,19 +9,12 @@
    overview
    api
 
-Finaloption is a command line parser, intended to make writing command line
+Opster is a command line parser, intended to make writing command line
 applications easy and painless. It uses built-in Python types (lists,
 dictionaries, etc) to define options, which makes configuration clear and
 concise. Additionally it contains possibility to handle subcommands (i.e.
 ``hg commit`` or ``svn update``).
 
-JFYI: name is derived from `Die Krupps'`_ song `Final Option`_, featured in
-epigraph.
-
-.. _Final Option: http://musi.cx/music/Die_Krupps/III_Odyssey_of_the_Mind/The_Final_Option/
-.. _Die Krupps': http://en.wikipedia.org/wiki/Die_Krupps
-
-
 Features
 --------
 
--- a/docs/make.bat	Fri Aug 07 19:23:13 2009 +0300
+++ b/docs/make.bat	Wed Aug 19 12:27:43 2009 +0300
@@ -73,9 +73,9 @@
 	echo.
 	echo.Build finished; now you can run "qcollectiongenerator" with the ^
 .qhcp project file in _build/qthelp, like this:
-	echo.^> qcollectiongenerator _build\qthelp\Finaloption.qhcp
+	echo.^> qcollectiongenerator _build\qthelp\Opster.qhcp
 	echo.To view the help file:
-	echo.^> assistant -collectionFile _build\qthelp\Finaloption.ghc
+	echo.^> assistant -collectionFile _build\qthelp\Opster.ghc
 	goto end
 )
 
--- a/docs/overview.rst	Fri Aug 07 19:23:13 2009 +0300
+++ b/docs/overview.rst	Wed Aug 19 12:27:43 2009 +0300
@@ -1,5 +1,5 @@
 ===================
- Finaloption usage
+ Opster usage
 ===================
 
 Options
@@ -31,7 +31,7 @@
 
 Usage is easy like that::
 
-  from finaloption import command
+  from opster import command
 
   @command(options=opts, usage='%name [-l HOST] DIR')
   def main(dirname, **opts):
@@ -81,7 +81,7 @@
 -----------
 
 It's pretty usual for complex application to have some system of subcommands,
-and finaloption provides facility for handling them. Configuration is simple::
+and opster provides facility for handling them. Configuration is simple::
 
   cmdtable = {
       '^simple':
@@ -124,7 +124,7 @@
 After definition of all elements you can call command dispatcher (``cmdtable``
 is defined earlier)::
 
-  from finaloption import dispatch
+  from opster import dispatch
 
   if __name__ == '__main__':
       dispatch(cmdtable=cmdtable)
--- a/finaloption.py	Fri Aug 07 19:23:13 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,500 +0,0 @@
-# (c) Alexander Solovyov, 2009, under terms of the new BSD License
-'''Command line arguments parser
-'''
-
-import sys, traceback, getopt, types, textwrap, inspect
-from itertools import imap
-
-__all__ = ['command', 'dispatch']
-__version__ = '0.9.6'
-__author__ = 'Alexander Solovyov'
-__email__ = 'piranha@piranha.org.ua'
-
-write = sys.stdout.write
-err = sys.stderr.write
-
-CMDTABLE = {}
-
-# --------
-# Public interface
-# --------
-
-def command(options=None, usage=None, name=None, shortlist=False, hide=False):
-    '''Decorator to mark function to be used for command line processing.
-
-    All arguments are optional:
-
-     - ``options``: options in format described in docs. If not supplied,
-       will be determined from function.
-     - ``usage``: usage string for function, replaces ``%name`` with name
-       of program or subcommand. In case if it's subcommand and ``%name``
-       is not present, usage is prepended by ``name``
-     - ``name``: used for multiple subcommands. Defaults to wrapped
-       function name
-     - ``shortlist``: if command should be included in shortlist. Used
-       only with multiple subcommands
-     - ``hide``: if command should be hidden from help listing. Used only
-       with multiple subcommands, overrides ``shortlist``
-    '''
-    def wrapper(func):
-        # copy option list
-        try:
-            options_ = list(options or guess_options(func))
-        except TypeError:
-            # no options supplied and no options present in func
-            options_ = []
-
-        name_ = name or func.__name__
-        usage_ = usage or guess_usage(func, options_)
-        prefix = hide and '~' or (shortlist and '^' or '')
-        CMDTABLE[prefix + name_] = (func, options_, usage_)
-
-        def help_func(name=None):
-            return help_cmd(func, replace_name(usage_, sysname()), options_)
-
-        @wraps(func)
-        def inner(*arguments, **kwarguments):
-            # look if we need to add 'help' option
-            try:
-                (True for option in reversed(options_)
-                 if option[1] == 'help').next()
-            except StopIteration:
-                options_.append(('h', 'help', False, 'show help'))
-
-            args = kwarguments.pop('args', None)
-            if arguments or kwarguments:
-                args, opts = arguments, kwarguments
-            else:
-                args = args or sys.argv[1:]
-                try:
-                    opts, args = catcher(lambda: parse(args, options_),
-                                         help_func)
-                except Abort:
-                    return -1
-
-            try:
-                if opts.pop('help', False):
-                    return help_func()
-                return catcher(lambda: call_cmd(name_, func, *args, **opts),
-                               help_func)
-            except Abort:
-                return -1
-
-        return inner
-    return wrapper
-
-
-def dispatch(args=None, cmdtable=None, globaloptions=None,
-             middleware=lambda x: x):
-    '''Dispatch command arguments based on subcommands.
-
-    - ``args``: list of arguments, default: ``sys.argv[1:]``
-    - ``cmdtable``: dict of commands in format described below.
-      If not supplied, will use functions decorated with ``@command``.
-    - ``globaloptions``: list of options which are applied to all
-      commands, will contain ``--help`` option at least.
-    - ``middleware``: global decorator for all commands.
-
-    cmdtable format description::
-
-      {'name': (function, options, usage)}
-
-    - ``name`` is the name used on command-line. Can contain aliases
-      (separate them with ``|``), pointer to a fact that this command
-      should be displayed in short help (start name with ``^``), or to
-      a fact that this command should be hidden (start name with ``~``)
-    - ``function`` is the actual callable
-    - ``options`` is options list in format described in docs
-    - ``usage`` is the short string of usage
-    '''
-    args = args or sys.argv[1:]
-    cmdtable = cmdtable or CMDTABLE
-
-    globaloptions = globaloptions or []
-    globaloptions.append(('h', 'help', False, 'display help'))
-
-    cmdtable['help'] = (help_(cmdtable, globaloptions), [], '[TOPIC]')
-    help_func = cmdtable['help'][0]
-
-    try:
-        name, func, args, kwargs = catcher(
-            lambda: _dispatch(args, cmdtable, globaloptions),
-            help_func)
-        return catcher(
-            lambda: call_cmd(name, middleware(func), *args, **kwargs),
-            help_func)
-    except Abort:
-        return -1
-
-# --------
-# Help
-# --------
-
-def help_(cmdtable, globalopts):
-    def help_inner(name=None):
-        '''Show help for a given help topic or a help overview
-
-        With no arguments, print a list of commands with short help messages.
-
-        Given a command name, print help for that command.
-        '''
-        def helplist():
-            hlp = {}
-            # determine if any command is marked for shortlist
-            shortlist = (name == 'shortlist' and
-                         any(imap(lambda x: x.startswith('^'), cmdtable)))
-
-            for cmd, info in cmdtable.items():
-                if cmd.startswith('~'):
-                    continue # do not display hidden commands
-                if shortlist and not cmd.startswith('^'):
-                    continue # short help contains only marked commands
-                cmd = cmd.lstrip('^~')
-                doc = info[0].__doc__ or '(no help text available)'
-                hlp[cmd] = doc.splitlines()[0].rstrip()
-
-            hlplist = sorted(hlp)
-            maxlen = max(map(len, hlplist))
-
-            write('usage: %s <command> [options]\n' % sysname())
-            write('\ncommands:\n\n')
-            for cmd in hlplist:
-                doc = hlp[cmd]
-                if False: # verbose?
-                    write(' %s:\n     %s\n' % (cmd.replace('|', ', '), doc))
-                else:
-                    write(' %-*s  %s\n' % (maxlen, cmd.split('|', 1)[0],
-                                              doc))
-
-        if not cmdtable:
-            return err('No commands specified!\n')
-
-        if not name or name == 'shortlist':
-            return helplist()
-
-        aliases, (cmd, options, usage) = findcmd(name, cmdtable)
-        return help_cmd(cmd,
-                        replace_name(usage, sysname() + ' ' + aliases[0]),
-                        options + globalopts)
-    return help_inner
-
-def help_cmd(func, usage, options):
-    '''show help for given command
-
-    - ``func``: function to generate help for (``func.__doc__`` is taken)
-    - ``usage``: usage string
-    - ``options``: options in usual format
-
-    >>> def test(*args, **opts):
-    ...     """that's a test command
-    ...
-    ...        you can do nothing with this command"""
-    ...     pass
-    >>> opts = [('l', 'listen', 'localhost',
-    ...          'ip to listen on'),
-    ...         ('p', 'port', 8000,
-    ...          'port to listen on'),
-    ...         ('d', 'daemonize', False,
-    ...          'daemonize process'),
-    ...         ('', 'pid-file', '',
-    ...          'name of file to write process ID to')]
-    >>> help_cmd(test, 'test [-l HOST] [NAME]', opts)
-    test [-l HOST] [NAME]
-    <BLANKLINE>
-    that's a test command
-    <BLANKLINE>
-           you can do nothing with this command
-    <BLANKLINE>
-    options:
-    <BLANKLINE>
-     -l --listen     ip to listen on (default: localhost)
-     -p --port       port to listen on (default: 8000)
-     -d --daemonize  daemonize process
-        --pid-file   name of file to write process ID to
-    <BLANKLINE>
-    '''
-    print '%s\n' % usage
-    doc = func.__doc__
-    if not doc:
-        doc = '(no help text available)'
-    print '%s\n' % doc.strip()
-    if options:
-        print ''.join(help_options(options))
-
-def help_options(options):
-    yield 'options:\n\n'
-    output = []
-    for short, name, default, desc in options:
-        default = default and ' (default: %s)' % default or ''
-        output.append(('%2s%s' % (short and '-%s' % short,
-                                  name and ' --%s' % name),
-                       '%s%s' % (desc, default)))
-
-    opts_len = max([len(first) for first, second in output if second] or [0])
-    for first, second in output:
-        if second:
-            # wrap description at 78 chars
-            second = textwrap.wrap(second, width=(78 - opts_len - 3))
-            pad = '\n' + ' ' * (opts_len + 3)
-            yield ' %-*s  %s\n' % (opts_len, first, pad.join(second))
-        else:
-            yield '%s\n' % first
-
-
-# --------
-# Options parsing
-# --------
-
-def parse(args, options):
-    '''
-    >>> opts = [('l', 'listen', 'localhost',
-    ...          'ip to listen on'),
-    ...         ('p', 'port', 8000,
-    ...          'port to listen on'),
-    ...         ('d', 'daemonize', False,
-    ...          'daemonize process'),
-    ...         ('', 'pid-file', '',
-    ...          'name of file to write process ID to')]
-    >>> print parse(['-l', '0.0.0.0', '--pi', 'test', 'all'], opts)
-    ({'pid_file': 'test', 'daemonize': False, 'port': 8000, 'listen': '0.0.0.0'}, ['all'])
-
-    '''
-    argmap, defmap, state = {}, {}, {}
-    shortlist, namelist = '', []
-
-    for short, name, default, comment in options:
-        if short and len(short) != 1:
-            raise FOError('Short option should be only a single'
-                          ' character: %s' % short)
-        if not name:
-            raise FOError(
-                'Long name should be defined for every option')
-        # change name to match Python styling
-        pyname = name.replace('-', '_')
-        argmap['-' + short] = argmap['--' + name] = pyname
-        defmap[pyname] = default
-
-        # copy defaults to state
-        if isinstance(default, list):
-            state[pyname] = default[:]
-        elif callable(default):
-            state[pyname] = None
-        else:
-            state[pyname] = default
-
-        # getopt wants indication that it takes a parameter
-        if not (default is None or default is True or default is False):
-            if short: short += ':'
-            if name: name += '='
-        if short:
-            shortlist += short
-        if name:
-            namelist.append(name)
-
-    opts, args = getopt.gnu_getopt(args, shortlist, namelist)
-
-    # transfer result to state
-    for opt, val in opts:
-        name = argmap[opt]
-        t = type(defmap[name])
-        if t is types.FunctionType:
-            state[name] = defmap[name](val)
-        elif t is types.IntType:
-            state[name] = int(val)
-        elif t is types.StringType:
-            state[name] = val
-        elif t is types.ListType:
-            state[name].append(val)
-        elif t in (types.NoneType, types.BooleanType):
-            state[name] = not defmap[name]
-
-    return state, args
-
-
-# --------
-# Subcommand system
-# --------
-
-def _dispatch(args, cmdtable, globalopts):
-    cmd, func, args, options, globaloptions = cmdparse(args, cmdtable,
-                                                       globalopts)
-
-    if globaloptions['help']:
-        return 'help', cmdtable['help'][0], [cmd], {}
-    elif not cmd:
-        return 'help', cmdtable['help'][0], ['shortlist'], {}
-
-    return cmd, func, args, options
-
-def cmdparse(args, cmdtable, globalopts):
-    # command is the first non-option
-    cmd = None
-    for arg in args:
-        if not arg.startswith('-'):
-            cmd = arg
-            break
-
-    if cmd:
-        args.pop(args.index(cmd))
-
-        aliases, info = findcmd(cmd, cmdtable)
-        cmd = aliases[0]
-        possibleopts = list(info[1])
-    else:
-        possibleopts = []
-
-    possibleopts.extend(globalopts)
-
-    try:
-        options, args = parse(args, possibleopts)
-    except getopt.GetoptError, e:
-        raise ParseError(cmd, e)
-
-    globaloptions = {}
-    for o in globalopts:
-        name = o[1]
-        globaloptions[name] = options.pop(name)
-
-    return (cmd, cmd and info[0] or None, args, options, globaloptions)
-
-def findpossible(cmd, table):
-    """
-    Return cmd -> (aliases, command table entry)
-    for each matching command.
-    """
-    choice = {}
-    for e in table.keys():
-        aliases = e.lstrip("^~").split("|")
-        found = None
-        if cmd in aliases:
-            found = cmd
-        else:
-            for a in aliases:
-                if a.startswith(cmd):
-                    found = a
-                    break
-        if found is not None:
-            choice[found] = (aliases, table[e])
-
-    return choice
-
-def findcmd(cmd, table):
-    """Return (aliases, command table entry) for command string."""
-    choice = findpossible(cmd, table)
-
-    if cmd in choice:
-        return choice[cmd]
-
-    if len(choice) > 1:
-        clist = choice.keys()
-        clist.sort()
-        raise AmbiguousCommand(cmd, clist)
-
-    if choice:
-        return choice.values()[0]
-
-    raise UnknownCommand(cmd)
-
-# --------
-# Helpers
-# --------
-
-def guess_options(func):
-    args, varargs, varkw, defaults = inspect.getargspec(func)
-    for lname, (sname, default, hlp) in zip(args[-len(defaults):], defaults):
-        yield (sname, lname.replace('_', '-'), default, hlp)
-
-def guess_usage(func, options):
-    usage = '%name '
-    if options:
-        usage += '[OPTIONS] '
-    args, varargs = inspect.getargspec(func)[:2]
-    argnum = len(args) - len(options)
-    if argnum > 0:
-        usage += args[0].upper()
-        if argnum > 1:
-            usage += 'S'
-    elif varargs:
-        usage += '[%s]' % varargs.upper()
-    return usage
-
-def catcher(target, help_func):
-    try:
-        return target()
-    except UnknownCommand, e:
-        err("unknown command: '%s'\n" % e)
-    except AmbiguousCommand, e:
-        err("command '%s' is ambiguous:\n    %s\n" %
-            (e.args[0], ' '.join(e.args[1])))
-    except ParseError, e:
-        err('%s: %s\n' % (e.args[0], e.args[1]))
-        help_func(e.args[0])
-    except getopt.GetoptError, e:
-        err('error: %s\n' % e)
-        help_func()
-    except FOError, e:
-        err('%s\n' % e)
-    except KeyboardInterrupt:
-        err('interrupted!\n')
-    except SystemExit:
-        raise
-    except:
-        err('unknown exception encountered')
-        raise
-
-    raise Abort
-
-def call_cmd(name, func, *args, **kwargs):
-    try:
-        return func(*args, **kwargs)
-    except TypeError:
-        if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
-            raise ParseError(name, "invalid arguments")
-        raise
-
-def replace_name(usage, name):
-    if '%name' in usage:
-        return usage.replace('%name', name, 1)
-    return name + ' ' + usage
-
-def sysname():
-    name = sys.argv[0]
-    if name.startswith('./'):
-        return name[2:]
-    return name
-
-try:
-    from functools import wraps
-except ImportError:
-    def wraps(wrapped, assigned=('__module__', '__name__', '__doc__'),
-              updated=('__dict__',)):
-        def inner(wrapper):
-            for attr in assigned:
-                setattr(wrapper, attr, getattr(wrapped, attr))
-            for attr in updated:
-                getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
-            return wrapper
-        return inner
-
-# --------
-# Exceptions
-# --------
-
-# Command exceptions
-class CommandException(Exception):
-    'Base class for command exceptions'
-
-class AmbiguousCommand(CommandException):
-    'Raised if command is ambiguous'
-
-class UnknownCommand(CommandException):
-    'Raised if command is unknown'
-
-class ParseError(CommandException):
-    'Raised on error in command line parsing'
-
-class Abort(CommandException):
-    'Abort execution'
-
-class FOError(CommandException):
-    'Raised on trouble with finaloption configuration'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/opster.py	Wed Aug 19 12:27:43 2009 +0300
@@ -0,0 +1,500 @@
+# (c) Alexander Solovyov, 2009, under terms of the new BSD License
+'''Command line arguments parser
+'''
+
+import sys, traceback, getopt, types, textwrap, inspect
+from itertools import imap
+
+__all__ = ['command', 'dispatch']
+__version__ = '0.9.6'
+__author__ = 'Alexander Solovyov'
+__email__ = 'piranha@piranha.org.ua'
+
+write = sys.stdout.write
+err = sys.stderr.write
+
+CMDTABLE = {}
+
+# --------
+# Public interface
+# --------
+
+def command(options=None, usage=None, name=None, shortlist=False, hide=False):
+    '''Decorator to mark function to be used for command line processing.
+
+    All arguments are optional:
+
+     - ``options``: options in format described in docs. If not supplied,
+       will be determined from function.
+     - ``usage``: usage string for function, replaces ``%name`` with name
+       of program or subcommand. In case if it's subcommand and ``%name``
+       is not present, usage is prepended by ``name``
+     - ``name``: used for multiple subcommands. Defaults to wrapped
+       function name
+     - ``shortlist``: if command should be included in shortlist. Used
+       only with multiple subcommands
+     - ``hide``: if command should be hidden from help listing. Used only
+       with multiple subcommands, overrides ``shortlist``
+    '''
+    def wrapper(func):
+        # copy option list
+        try:
+            options_ = list(options or guess_options(func))
+        except TypeError:
+            # no options supplied and no options present in func
+            options_ = []
+
+        name_ = name or func.__name__
+        usage_ = usage or guess_usage(func, options_)
+        prefix = hide and '~' or (shortlist and '^' or '')
+        CMDTABLE[prefix + name_] = (func, options_, usage_)
+
+        def help_func(name=None):
+            return help_cmd(func, replace_name(usage_, sysname()), options_)
+
+        @wraps(func)
+        def inner(*arguments, **kwarguments):
+            # look if we need to add 'help' option
+            try:
+                (True for option in reversed(options_)
+                 if option[1] == 'help').next()
+            except StopIteration:
+                options_.append(('h', 'help', False, 'show help'))
+
+            args = kwarguments.pop('args', None)
+            if arguments or kwarguments:
+                args, opts = arguments, kwarguments
+            else:
+                args = args or sys.argv[1:]
+                try:
+                    opts, args = catcher(lambda: parse(args, options_),
+                                         help_func)
+                except Abort:
+                    return -1
+
+            try:
+                if opts.pop('help', False):
+                    return help_func()
+                return catcher(lambda: call_cmd(name_, func, *args, **opts),
+                               help_func)
+            except Abort:
+                return -1
+
+        return inner
+    return wrapper
+
+
+def dispatch(args=None, cmdtable=None, globaloptions=None,
+             middleware=lambda x: x):
+    '''Dispatch command arguments based on subcommands.
+
+    - ``args``: list of arguments, default: ``sys.argv[1:]``
+    - ``cmdtable``: dict of commands in format described below.
+      If not supplied, will use functions decorated with ``@command``.
+    - ``globaloptions``: list of options which are applied to all
+      commands, will contain ``--help`` option at least.
+    - ``middleware``: global decorator for all commands.
+
+    cmdtable format description::
+
+      {'name': (function, options, usage)}
+
+    - ``name`` is the name used on command-line. Can contain aliases
+      (separate them with ``|``), pointer to a fact that this command
+      should be displayed in short help (start name with ``^``), or to
+      a fact that this command should be hidden (start name with ``~``)
+    - ``function`` is the actual callable
+    - ``options`` is options list in format described in docs
+    - ``usage`` is the short string of usage
+    '''
+    args = args or sys.argv[1:]
+    cmdtable = cmdtable or CMDTABLE
+
+    globaloptions = globaloptions or []
+    globaloptions.append(('h', 'help', False, 'display help'))
+
+    cmdtable['help'] = (help_(cmdtable, globaloptions), [], '[TOPIC]')
+    help_func = cmdtable['help'][0]
+
+    try:
+        name, func, args, kwargs = catcher(
+            lambda: _dispatch(args, cmdtable, globaloptions),
+            help_func)
+        return catcher(
+            lambda: call_cmd(name, middleware(func), *args, **kwargs),
+            help_func)
+    except Abort:
+        return -1
+
+# --------
+# Help
+# --------
+
+def help_(cmdtable, globalopts):
+    def help_inner(name=None):
+        '''Show help for a given help topic or a help overview
+
+        With no arguments, print a list of commands with short help messages.
+
+        Given a command name, print help for that command.
+        '''
+        def helplist():
+            hlp = {}
+            # determine if any command is marked for shortlist
+            shortlist = (name == 'shortlist' and
+                         any(imap(lambda x: x.startswith('^'), cmdtable)))
+
+            for cmd, info in cmdtable.items():
+                if cmd.startswith('~'):
+                    continue # do not display hidden commands
+                if shortlist and not cmd.startswith('^'):
+                    continue # short help contains only marked commands
+                cmd = cmd.lstrip('^~')
+                doc = info[0].__doc__ or '(no help text available)'
+                hlp[cmd] = doc.splitlines()[0].rstrip()
+
+            hlplist = sorted(hlp)
+            maxlen = max(map(len, hlplist))
+
+            write('usage: %s <command> [options]\n' % sysname())
+            write('\ncommands:\n\n')
+            for cmd in hlplist:
+                doc = hlp[cmd]
+                if False: # verbose?
+                    write(' %s:\n     %s\n' % (cmd.replace('|', ', '), doc))
+                else:
+                    write(' %-*s  %s\n' % (maxlen, cmd.split('|', 1)[0],
+                                              doc))
+
+        if not cmdtable:
+            return err('No commands specified!\n')
+
+        if not name or name == 'shortlist':
+            return helplist()
+
+        aliases, (cmd, options, usage) = findcmd(name, cmdtable)
+        return help_cmd(cmd,
+                        replace_name(usage, sysname() + ' ' + aliases[0]),
+                        options + globalopts)
+    return help_inner
+
+def help_cmd(func, usage, options):
+    '''show help for given command
+
+    - ``func``: function to generate help for (``func.__doc__`` is taken)
+    - ``usage``: usage string
+    - ``options``: options in usual format
+
+    >>> def test(*args, **opts):
+    ...     """that's a test command
+    ...
+    ...        you can do nothing with this command"""
+    ...     pass
+    >>> opts = [('l', 'listen', 'localhost',
+    ...          'ip to listen on'),
+    ...         ('p', 'port', 8000,
+    ...          'port to listen on'),
+    ...         ('d', 'daemonize', False,
+    ...          'daemonize process'),
+    ...         ('', 'pid-file', '',
+    ...          'name of file to write process ID to')]
+    >>> help_cmd(test, 'test [-l HOST] [NAME]', opts)
+    test [-l HOST] [NAME]
+    <BLANKLINE>
+    that's a test command
+    <BLANKLINE>
+           you can do nothing with this command
+    <BLANKLINE>
+    options:
+    <BLANKLINE>
+     -l --listen     ip to listen on (default: localhost)
+     -p --port       port to listen on (default: 8000)
+     -d --daemonize  daemonize process
+        --pid-file   name of file to write process ID to
+    <BLANKLINE>
+    '''
+    print '%s\n' % usage
+    doc = func.__doc__
+    if not doc:
+        doc = '(no help text available)'
+    print '%s\n' % doc.strip()
+    if options:
+        print ''.join(help_options(options))
+
+def help_options(options):
+    yield 'options:\n\n'
+    output = []
+    for short, name, default, desc in options:
+        default = default and ' (default: %s)' % default or ''
+        output.append(('%2s%s' % (short and '-%s' % short,
+                                  name and ' --%s' % name),
+                       '%s%s' % (desc, default)))
+
+    opts_len = max([len(first) for first, second in output if second] or [0])
+    for first, second in output:
+        if second:
+            # wrap description at 78 chars
+            second = textwrap.wrap(second, width=(78 - opts_len - 3))
+            pad = '\n' + ' ' * (opts_len + 3)
+            yield ' %-*s  %s\n' % (opts_len, first, pad.join(second))
+        else:
+            yield '%s\n' % first
+
+
+# --------
+# Options parsing
+# --------
+
+def parse(args, options):
+    '''
+    >>> opts = [('l', 'listen', 'localhost',
+    ...          'ip to listen on'),
+    ...         ('p', 'port', 8000,
+    ...          'port to listen on'),
+    ...         ('d', 'daemonize', False,
+    ...          'daemonize process'),
+    ...         ('', 'pid-file', '',
+    ...          'name of file to write process ID to')]
+    >>> print parse(['-l', '0.0.0.0', '--pi', 'test', 'all'], opts)
+    ({'pid_file': 'test', 'daemonize': False, 'port': 8000, 'listen': '0.0.0.0'}, ['all'])
+
+    '''
+    argmap, defmap, state = {}, {}, {}
+    shortlist, namelist = '', []
+
+    for short, name, default, comment in options:
+        if short and len(short) != 1:
+            raise FOError('Short option should be only a single'
+                          ' character: %s' % short)
+        if not name:
+            raise FOError(
+                'Long name should be defined for every option')
+        # change name to match Python styling
+        pyname = name.replace('-', '_')
+        argmap['-' + short] = argmap['--' + name] = pyname
+        defmap[pyname] = default
+
+        # copy defaults to state
+        if isinstance(default, list):
+            state[pyname] = default[:]
+        elif callable(default):
+            state[pyname] = None
+        else:
+            state[pyname] = default
+
+        # getopt wants indication that it takes a parameter
+        if not (default is None or default is True or default is False):
+            if short: short += ':'
+            if name: name += '='
+        if short:
+            shortlist += short
+        if name:
+            namelist.append(name)
+
+    opts, args = getopt.gnu_getopt(args, shortlist, namelist)
+
+    # transfer result to state
+    for opt, val in opts:
+        name = argmap[opt]
+        t = type(defmap[name])
+        if t is types.FunctionType:
+            state[name] = defmap[name](val)
+        elif t is types.IntType:
+            state[name] = int(val)
+        elif t is types.StringType:
+            state[name] = val
+        elif t is types.ListType:
+            state[name].append(val)
+        elif t in (types.NoneType, types.BooleanType):
+            state[name] = not defmap[name]
+
+    return state, args
+
+
+# --------
+# Subcommand system
+# --------
+
+def _dispatch(args, cmdtable, globalopts):
+    cmd, func, args, options, globaloptions = cmdparse(args, cmdtable,
+                                                       globalopts)
+
+    if globaloptions['help']:
+        return 'help', cmdtable['help'][0], [cmd], {}
+    elif not cmd:
+        return 'help', cmdtable['help'][0], ['shortlist'], {}
+
+    return cmd, func, args, options
+
+def cmdparse(args, cmdtable, globalopts):
+    # command is the first non-option
+    cmd = None
+    for arg in args:
+        if not arg.startswith('-'):
+            cmd = arg
+            break
+
+    if cmd:
+        args.pop(args.index(cmd))
+
+        aliases, info = findcmd(cmd, cmdtable)
+        cmd = aliases[0]
+        possibleopts = list(info[1])
+    else:
+        possibleopts = []
+
+    possibleopts.extend(globalopts)
+
+    try:
+        options, args = parse(args, possibleopts)
+    except getopt.GetoptError, e:
+        raise ParseError(cmd, e)
+
+    globaloptions = {}
+    for o in globalopts:
+        name = o[1]
+        globaloptions[name] = options.pop(name)
+
+    return (cmd, cmd and info[0] or None, args, options, globaloptions)
+
+def findpossible(cmd, table):
+    """
+    Return cmd -> (aliases, command table entry)
+    for each matching command.
+    """
+    choice = {}
+    for e in table.keys():
+        aliases = e.lstrip("^~").split("|")
+        found = None
+        if cmd in aliases:
+            found = cmd
+        else:
+            for a in aliases:
+                if a.startswith(cmd):
+                    found = a
+                    break
+        if found is not None:
+            choice[found] = (aliases, table[e])
+
+    return choice
+
+def findcmd(cmd, table):
+    """Return (aliases, command table entry) for command string."""
+    choice = findpossible(cmd, table)
+
+    if cmd in choice:
+        return choice[cmd]
+
+    if len(choice) > 1:
+        clist = choice.keys()
+        clist.sort()
+        raise AmbiguousCommand(cmd, clist)
+
+    if choice:
+        return choice.values()[0]
+
+    raise UnknownCommand(cmd)
+
+# --------
+# Helpers
+# --------
+
+def guess_options(func):
+    args, varargs, varkw, defaults = inspect.getargspec(func)
+    for lname, (sname, default, hlp) in zip(args[-len(defaults):], defaults):
+        yield (sname, lname.replace('_', '-'), default, hlp)
+
+def guess_usage(func, options):
+    usage = '%name '
+    if options:
+        usage += '[OPTIONS] '
+    args, varargs = inspect.getargspec(func)[:2]
+    argnum = len(args) - len(options)
+    if argnum > 0:
+        usage += args[0].upper()
+        if argnum > 1:
+            usage += 'S'
+    elif varargs:
+        usage += '[%s]' % varargs.upper()
+    return usage
+
+def catcher(target, help_func):
+    try:
+        return target()
+    except UnknownCommand, e:
+        err("unknown command: '%s'\n" % e)
+    except AmbiguousCommand, e:
+        err("command '%s' is ambiguous:\n    %s\n" %
+            (e.args[0], ' '.join(e.args[1])))
+    except ParseError, e:
+        err('%s: %s\n' % (e.args[0], e.args[1]))
+        help_func(e.args[0])
+    except getopt.GetoptError, e:
+        err('error: %s\n' % e)
+        help_func()
+    except FOError, e:
+        err('%s\n' % e)
+    except KeyboardInterrupt:
+        err('interrupted!\n')
+    except SystemExit:
+        raise
+    except:
+        err('unknown exception encountered')
+        raise
+
+    raise Abort
+
+def call_cmd(name, func, *args, **kwargs):
+    try:
+        return func(*args, **kwargs)
+    except TypeError:
+        if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
+            raise ParseError(name, "invalid arguments")
+        raise
+
+def replace_name(usage, name):
+    if '%name' in usage:
+        return usage.replace('%name', name, 1)
+    return name + ' ' + usage
+
+def sysname():
+    name = sys.argv[0]
+    if name.startswith('./'):
+        return name[2:]
+    return name
+
+try:
+    from functools import wraps
+except ImportError:
+    def wraps(wrapped, assigned=('__module__', '__name__', '__doc__'),
+              updated=('__dict__',)):
+        def inner(wrapper):
+            for attr in assigned:
+                setattr(wrapper, attr, getattr(wrapped, attr))
+            for attr in updated:
+                getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
+            return wrapper
+        return inner
+
+# --------
+# Exceptions
+# --------
+
+# Command exceptions
+class CommandException(Exception):
+    'Base class for command exceptions'
+
+class AmbiguousCommand(CommandException):
+    'Raised if command is ambiguous'
+
+class UnknownCommand(CommandException):
+    'Raised if command is unknown'
+
+class ParseError(CommandException):
+    'Raised on error in command line parsing'
+
+class Abort(CommandException):
+    'Abort execution'
+
+class FOError(CommandException):
+    'Raised on trouble with opster configuration'
--- a/setup.py	Fri Aug 07 19:23:13 2009 +0300
+++ b/setup.py	Wed Aug 19 12:27:43 2009 +0300
@@ -2,7 +2,7 @@
 
 import os
 from distutils.core import setup
-import finaloption
+import opster
 
 def read(fname):
     return open(os.path.join(os.path.dirname(__file__), fname)).read()
@@ -16,14 +16,14 @@
         return info
 
 setup(
-    name = 'finaloption',
-    description = 'command line parsing done right',
+    name = 'opster',
+    description = 'command line parsing speedster',
     long_description = desc(),
     license = 'BSD',
-    version = finaloption.__version__,
-    author = finaloption.__author__,
-    author_email = finaloption.__email__,
-    url = 'http://hg.piranha.org.ua/finaloption/',
+    version = opster.__version__,
+    author = opster.__author__,
+    author_email = opster.__email__,
+    url = 'http://hg.piranha.org.ua/opster/',
     classifiers = [
         'Development Status :: 4 - Beta',
         'Environment :: Console',
@@ -33,6 +33,6 @@
         'Programming Language :: Python',
         'Topic :: Software Development',
         ],
-    py_modules = ['finaloption'],
+    py_modules = ['opster'],
     platforms='any',
     )
--- a/test.py	Fri Aug 07 19:23:13 2009 +0300
+++ b/test.py	Wed Aug 19 12:27:43 2009 +0300
@@ -2,7 +2,7 @@
 
 import sys
 
-from finaloption import dispatch, command
+from opster import dispatch, command
 
 
 @command(usage='[-t]', shortlist=True)
--- a/test_cmd.py	Fri Aug 07 19:23:13 2009 +0300
+++ b/test_cmd.py	Wed Aug 19 12:27:43 2009 +0300
@@ -1,17 +1,17 @@
 #!/usr/bin/env python
 
-import finaloption
+import opster
 
 config_opts=[('c', 'config', 'webshops.ini', 'config file to use')]
 
 
-@finaloption.command(config_opts)
+@opster.command(config_opts)
 def initdb(config):
     """Initialize database"""
     pass
 
 
-@finaloption.command(options=config_opts + [
+@opster.command(options=config_opts + [
     ('h', 'host', 'localhost', 'The host for the application.'),
     ('p', 'port', 5000, 'The port for the server.'),
     ('', 'nolint', False, 'Do not use LintMiddleware')
@@ -21,4 +21,4 @@
     print opts
 
 
-finaloption.dispatch()
+opster.dispatch()
--- a/test_opts.py	Fri Aug 07 19:23:13 2009 +0300
+++ b/test_opts.py	Wed Aug 19 12:27:43 2009 +0300
@@ -2,7 +2,7 @@
 
 import sys
 
-from finaloption import command
+from opster import command
 
 opts = [('l', 'listen', 'localhost', 'ip to listen on'),
         ('p', 'port', 8000, 'port to listen on'),