--- a/fancycmd.py Thu Jul 02 22:17:21 2009 +0300
+++ b/fancycmd.py Mon Jul 13 00:48:07 2009 +0300
@@ -2,49 +2,74 @@
'''Fancy command line arguments parser
'''
-import sys, traceback, getopt, types, textwrap
+import sys, traceback, getopt, types, textwrap, inspect
from itertools import imap
-__all__ = ['fancyopts', 'dispatch', 'optionize']
+__all__ = ['command', 'dispatch']
+
+write = sys.stdout.write
+err = sys.stderr.write
+
+CMDTABLE = {}
# --------
# Public interface
# --------
-def optionize(options, usage):
- if '%prog' in usage.split():
- name = sys.argv[0]
- if name.startswith('./'):
- name = name[2:]
- usage = usage.replace('%prog', name, 1)
+def command(options=None, usage='', name=None, shortlist=False):
+ '''Mark function to be used for command line processing.
+ '''
+ def wrapper(func):
+ if '%prog' in usage.split():
+ name_ = sys.argv[0]
+ if name_.startswith('./'):
+ name_ = name_[2:]
+ usage_ = usage.replace('%prog', name_, 1)
+ else:
+ name_ = name or func.__name__
+ usage_ = name_ + ': ' + usage
+ options_ = options or list(guess_options(func))
+ options_.append(('h', 'help', False, 'show help'))
+
+ CMDTABLE[(shortlist and '^' or '') + name_] = (
+ func, options_, usage_)
- def wrapper(cmd):
- def inner():
- args = sys.argv[1:]
- return fancyopts(cmd, options, usage)(args)
+ def help_func(name=None):
+ return help_cmd(func, usage_, options_)
+
+ def inner(args=None):
+ args = args or sys.argv[1:]
+ if not args:
+ return help_func()
+
+ try:
+ opts, args = catcher(lambda: parse(args, options_), help_func)
+ 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 fancyopts(cmd, options, usage):
- def inner(args):
- if not args:
- return help_cmd(cmd, usage, options)
- opts, args = parse(args, options)
- return cmd(*args, **opts)
- return inner
-def dispatch(args, cmdtable, globalopts=None):
+def dispatch(args=None, cmdtable=None, globalopts=None):
'''Dispatch command arguments based on subcommands.
- - ``args``: sys.argv[1:]
+ - ``args``: list of arguments, default: sys.argv[1:]
- ``cmdtable``: dict of commands in next format::
{'name': (function, options, usage)}
+ if not supplied, functions decorated with @command will be used.
+
- ``globalopts``: list of options which are applied to all
commands, if not supplied will contain ``--help`` option
- where:
+ cmdtable format description follows:
- ``name`` is the name used on command-line. Can containt
aliases (separate them with '|') or pointer to the fact
@@ -54,45 +79,32 @@
- ``options`` is options list in fancyopts format
- ``usage`` is the short string of usage
'''
+ args = args or sys.argv[1:]
+ cmdtable = cmdtable or CMDTABLE
- ui = UI()
- if not globalopts:
- globalopts = [
- ('h', 'help', False, 'display help'),
- # is not used yet
- ('', 'traceback', False, 'display full traceback on error')]
+ globalopts = globalopts or []
+ globalopts.append(('h', 'help', False, 'display help'))
cmdtable['help'] = (help_(cmdtable, globalopts), [], '[TOPIC]')
+ help_func = cmdtable['help'][0]
try:
- return _dispatch(ui, args, cmdtable, globalopts + UI.options)
- except Abort, e:
- ui.warn('abort: %s\n' % e)
- except UnknownCommand, e:
- ui.warn("unknown command: '%s'\n" % e)
- except AmbiguousCommand, e:
- ui.warn("command '%s' is ambiguous:\n %s\n" %
- (e.args[0], ' '.join(e.args[1])))
- except ParseError, e:
- ui.warn('%s: %s\n' % (e.args[0], e.args[1]))
- cmdtable['help'][0](ui, e.args[0])
- except KeyboardInterrupt:
- ui.warn('interrupted!\n')
- except SystemExit:
- raise
- except:
- ui.warn('unknown exception encountered')
- raise
-
+ name, func, args, kwargs = catcher(
+ lambda: _dispatch(args, cmdtable, globalopts),
+ help_func)
+ return catcher(
+ lambda: call_cmd(name, func, *args, **kwargs),
+ help_func)
+ except Abort:
+ pass
return -1
-
# --------
# Help
# --------
def help_(cmdtable, globalopts):
- def inner(ui, name=None):
+ def 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.
@@ -116,14 +128,14 @@
maxlen = max(map(len, hlplist))
for cmd in hlplist:
doc = hlp[cmd]
- if ui.verbose:
- ui.write(' %s:\n %s\n' % (cmd.replace('|', ', '), doc))
+ if False: # verbose?
+ write(' %s:\n %s\n' % (cmd.replace('|', ', '), doc))
else:
- ui.write(' %-*s %s\n' % (maxlen, cmd.split('|', 1)[0],
+ write(' %-*s %s\n' % (maxlen, cmd.split('|', 1)[0],
doc))
if not cmdtable:
- return ui.warn('No commands specified!\n')
+ return err('No commands specified!\n')
if not name or name == 'shortlist':
return helplist()
@@ -259,25 +271,16 @@
# Subcommand system
# --------
-def _dispatch(ui, args, cmdtable, globalopts):
+def _dispatch(args, cmdtable, globalopts):
cmd, func, args, options, globaloptions = cmdparse(args, cmdtable,
globalopts)
- ui.verbose = globaloptions['verbose']
- # see UI.__init__ for explanation
- ui.quiet = (not ui.verbose and globaloptions['quiet'])
-
if globaloptions['help']:
- return cmdtable['help'][0](ui, cmd)
+ return 'help', cmdtable['help'][0], [cmd], {}
elif not cmd:
- return cmdtable['help'][0](ui, 'shortlist')
+ return 'help', cmdtable['help'][0], ['shortlist'], {}
- try:
- return func(ui, *args, **options)
- except TypeError:
- if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
- raise ParseError(cmd, "invalid arguments")
- raise
+ return cmd, func, args, options
def cmdparse(args, cmdtable, globalopts):
# command is the first non-option
@@ -348,51 +351,56 @@
raise UnknownCommand(cmd)
-
# --------
-# UI and exceptions
+# Helpers
# --------
-class UI(object):
- '''User interface helper.
-
- Intended to ease handling of quiet/verbose output and more.
-
- You have three methods to handle program messages output:
+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, default, hlp)
- - ``UI.info`` is printed by default, but hidden with quiet option
- - ``UI.note`` is printed only if output is verbose
- - ``UI.write`` is printed in any case
-
- Additionally there is ``UI.warn`` method, which prints to stderr.
- '''
- options = [('v', 'verbose', False, 'enable additional output'),
- ('q', 'quiet', False, 'suppress output')]
-
- def __init__(self, verbose=False, quiet=False):
- self.verbose = verbose
- # disabling quiet in favor of verbose is more safe
- self.quiet = (not verbose and quiet)
+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 KeyboardInterrupt:
+ err('interrupted!\n')
+ except SystemExit:
+ raise
+ except:
+ err('unknown exception encountered')
+ raise
- def write(self, *messages):
- for m in messages:
- sys.stdout.write(m)
+ raise Abort
- def warn(self, *messages):
- for m in messages:
- sys.stderr.write(m)
+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
- info = lambda self, *m: not self.quiet and self.write(*m)
- note = lambda self, *m: self.verbose and self.write(*m)
+# --------
+# Exceptions
+# --------
# Command exceptions
class CommandException(Exception):
'Base class for command exceptions'
-class Abort(CommandException):
- 'Raised if an error in command occured'
-
class AmbiguousCommand(CommandException):
'Raised if command is ambiguous'
@@ -405,6 +413,5 @@
class SignatureError(CommandException):
'Raised if function signature does not correspond to arguments'
-if __name__ == '__main__':
- import doctest
- doctest.testmod()
+class Abort(CommandException):
+ 'Abort execution'
--- a/test.py Thu Jul 02 22:17:21 2009 +0300
+++ b/test.py Mon Jul 13 00:48:07 2009 +0300
@@ -2,17 +2,22 @@
import sys
-from fancycmd import dispatch
+from fancycmd import dispatch, command
-def simple(ui, *args, **opts):
+@command(usage='[-t]', shortlist=True)
+def simple(test=('t', False, 'just test execution')):
'''Just simple command to do nothing.
I assure you! Nothing to look here. ;-)
'''
- print opts
+ print locals()
-def complex_(ui, *args, **opts):
+cplx_opts = [('p', 'pass', False, 'don\'t run the command'),
+ ('', 'exit', 0, 'exit with supplied code (default: 0)')]
+
+@command(cplx_opts, usage='[-p] [--exit value] ...', name='complex')
+def complex_(*args, **opts):
'''That's more complex command indented to do something
Let's try to do that (damn, but what?!)
@@ -20,24 +25,8 @@
if opts.get('pass'):
return
# test ui
- ui.write('what the?!\n')
- ui.warn('this is stderr\n')
- ui.status('this would be invisible in quiet mode\n')
- ui.note('this would be visible only in verbose mode\n')
- ui.write('%s, %s\n' % (args, opts))
if opts.get('exit'):
sys.exit(opts['exit'])
-cmdtable = {
- '^simple':
- (simple,
- [('t', 'test', False, 'just test execution')],
- '[-t] ...'),
- 'complex|hard':
- (complex_,
- [('p', 'pass', False, 'don\'t run the command'),
- ('', 'exit', 0, 'exit with supplied code (default: 0)')],
- '[-p] [--exit value] ...')}
-
if __name__ == '__main__':
- dispatch(sys.argv[1:], cmdtable)
+ dispatch()
--- a/test_opts.py Thu Jul 02 22:17:21 2009 +0300
+++ b/test_opts.py Mon Jul 13 00:48:07 2009 +0300
@@ -2,20 +2,33 @@
import sys
-from fancycmd import optionize
+from fancycmd import command
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')]
-@optionize(opts, usage='%prog [-l HOST] DIR')
+@command(opts, usage='%prog [-l HOST] DIR')
def main(dirname, **opts):
'''This is some command
It looks very similar to some serve command
'''
- print opts.get('pid_file')
+ print opts
+
+@command(usage='%prog [-l HOST] DIR')
+def another(dirname,
+ listen=('l', 'localhost', 'ip to listen on'),
+ port=('p', 8000, 'port to listen on'),
+ daemonize=('d', False, 'daemonize process'),
+ pid_file=('', '', 'name of file to write process ID to')):
+ '''Command with option declaration as keyword arguments
+
+ Otherwise it's the same as previons command
+ '''
+ print locals()
if __name__ == '__main__':
- main()
+ #main()
+ another()