--- a/fancycmd.py Sat Jun 27 20:58:12 2009 +0300
+++ b/fancycmd.py Sat Jun 27 22:17:05 2009 +0300
@@ -1,7 +1,80 @@
-import sys, traceback, getopt
+# (c) Alexander Solovyov, 2009, under terms of the new BSD License
+'''Fancy command line arguments parser
+'''
+
+import sys, traceback, getopt, types, textwrap
from itertools import imap
-from fancyopts import parse, cmd_help
+__all__ = ['fancyopts', 'dispatch']
+
+# --------
+# Public interface
+# --------
+
+def fancyopts(cmd, usage, options, args):
+ if not args:
+ cmd_help(cmd, usage, options)
+ else:
+ opts, args = parse(args, options)
+ cmd(*args, **opts)
+
+def dispatch(args, cmdtable, globalopts=None):
+ '''Dispatch command arguments based on subcommands.
+
+ - ``args``: sys.argv[1:]
+ - ``cmdtable``: dict of commands in next format::
+
+ {'name': (function, options, usage)}
+
+ - ``globalopts``: list of options which are applied to all
+ commands, if not supplied will contain ``--help`` option
+
+ where:
+
+ - ``name`` is the name used on command-line. Can containt
+ aliases (separate them with '|') or pointer to the fact
+ that this command should be displayed in short help (start
+ name with '^')
+ - ``function`` is the actual callable
+ - ``options`` is options list in fancyopts format
+ - ``usage`` is the short string of usage
+ '''
+
+ ui = UI()
+ if not globalopts:
+ globalopts = [
+ ('h', 'help', False, 'display help'),
+ # is not used yet
+ ('', 'traceback', False, 'display full traceback on error')]
+
+ cmdtable['help'] = (help_(cmdtable, globalopts), [], '[TOPIC]')
+
+ 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
+
+ return -1
+
+
+# --------
+# Help
+# --------
def help_(cmdtable, globalopts):
def inner(ui, name=None):
@@ -44,57 +117,132 @@
return cmd_help(cmd, aliases[0] + ' ' + usage, options)
return inner
-def dispatch(args, cmdtable, globalopts=None):
- '''Dispatch command arguments based on subcommands.
-
- - ``args``: sys.argv[1:]
- - ``cmdtable``: dict of commands in next format::
-
- {'name': (function, options, usage)}
-
- - ``globalopts``: list of options which are applied to all
- commands, if not supplied will contain ``--help`` option
+def cmd_help(cmd, usage, options):
+ '''show help for given command
- where:
+ >>> 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')]
+ >>> cmd_help(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 = cmd.__doc__
+ if not doc:
+ doc = '(no help text available)'
+ print '%s\n' % doc.strip()
+ if options:
+ print ''.join(help_options(options))
- - ``name`` is the name used on command-line. Can containt
- aliases (separate them with '|') or pointer to the fact
- that this command should be displayed in short help (start
- name with '^')
- - ``function`` is the actual callable
- - ``options`` is options list in fancyopts format
- - ``usage`` is the short string of usage
- '''
+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 70 chars
+ second = textwrap.wrap(second, width=(70 - 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
+# --------
- ui = UI()
- if not globalopts:
- globalopts = [
- ('h', 'help', False, 'display help'),
- ('', 'traceback', False, 'display full traceback on error')]
+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'])
- cmdtable['help'] = (help_(cmdtable, globalopts), [], '[TOPIC]')
+ '''
+ argmap, defmap, state = {}, {}, {}
+ shortlist, namelist = '', []
+
+ for short, name, default, comment in options:
+ # 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 hasattr(default, '__call__'):
+ state[pyname] = None
+ else:
+ state[pyname] = default
- 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
+ # 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)
- return -1
+ # 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(ui, args, cmdtable, globalopts):
cmd, func, args, options, globaloptions = cmdparse(args, cmdtable,
@@ -185,6 +333,11 @@
raise UnknownCommand(cmd)
+
+# --------
+# UI and exceptions
+# --------
+
class UI(object):
'''User interface helper.
@@ -237,3 +390,6 @@
class SignatureError(CommandException):
'Raised if function signature does not correspond to arguments'
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
--- a/fancyopts.py Sat Jun 27 20:58:12 2009 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,173 +0,0 @@
-# (c) Alexander Solovyov, 2009, under terms of the new BSD License
-'''Fancy option parser
-
-Usage::
-
- >>> def serve(dirname, **opts):
- ... """this is a do-nothing command
- ...
- ... you can do nothing with this command"""
- ... print opts.get('listen'), opts.get('pid_file')
- >>> 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')]
- >>> from fancyopts import fancyopts
- >>> fancyopts(serve, 'serve [-l HOST] DIR', opts, '--pid-f test dir'.split())
- localhost test
-
-You have supplied directory name here and path to file with process id.
-Order of options is preserved.
-
-Each option definition is a tuple consisting of 4 elements:
-
- - short name
- - long name
- - default value
- - help string
-
-Default value determines option type, which is selected from this choices:
-
- - function: return value of function called with a specified value is passed
- - integer: value is convert to integer
- - string: value is passed as is
- - list: value is appended to this list
- - boolean/None: `not default` is passed
-'''
-
-import getopt, types, textwrap
-
-__all__ = ['fancyopts']
-
-def fancyopts(cmd, usage, options, args):
- if not args:
- cmd_help(cmd, usage, options)
- else:
- opts, args = parse(args, options)
- cmd(*args, **opts)
-
-def cmd_help(cmd, usage, options):
- '''show help for given command
-
- >>> 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')]
- >>> cmd_help(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 = cmd.__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 70 chars
- second = textwrap.wrap(second, width=(70 - opts_len - 3))
- pad = '\n' + ' ' * (opts_len + 3)
- yield ' %-*s %s\n' % (opts_len, first, pad.join(second))
- else:
- yield '%s\n' % first
-
-
-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:
- # 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 hasattr(default, '__call__'):
- 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
-
-if __name__ == '__main__':
- import doctest
- doctest.testmod()
--- a/test.py Sat Jun 27 20:58:12 2009 +0300
+++ b/test.py Sat Jun 27 22:17:05 2009 +0300
@@ -37,7 +37,7 @@
(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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test_opts.py Sat Jun 27 22:17:05 2009 +0300
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+import sys
+
+from fancycmd import fancyopts
+
+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')]
+
+def main(dirname, **opts):
+ '''This is some command
+
+ It looks very similar to some serve command
+ '''
+ print opts.get('pid_file')
+
+if __name__ == '__main__':
+ fancyopts(main, '%s [-l HOST] DIR' % sys.argv[0], opts, sys.argv[1:])