@@ 10,21 10,25 @@ GOPT_A = [
def cmd1(fn, **opts):
+ """the number one operation
+
+ This produces the number one. Always.
+ """
return 1
def cmd2(**opts):
return 2
-def cmd3():
+def cmd3(**opts):
return 3
CMDTABLE_A = [
- ('foo', cmd1, '', [], 'foo fixes everything'),
+ ('foo', cmd1, '', [], 'FILENAME'),
('bar', cmd2, '', [], 'bar breaks it'),
('baz', cmd3, 'wibble|hamper', [
('shrimp', '', None, 'jumbo shrimp'),
- ], 'something helpful'),
+ ], ''),
]
CMDTABLE_B = [
@@ 145,5 149,71 @@ class TestCliCommands(unittest.TestCase)
commander.launch, GOPT_A, CMDTABLE_A, arguments)
+ def test_simple_usage(self):
+ arguments = 'foo test.txt'.split()
+ cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+ usage = '\n'.join(commander.usage(cmd))
+ self.assert_('FILENAME' in usage)
+ self.assert_('number one' in usage)
+ self.assert_('Always' in usage)
+
+ arguments = 'baz test.txt'.split()
+ cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+ usage = '\n'.join(commander.usage(cmd))
+ self.assert_('--shrimp' in usage)
+ self.assert_('jumbo shrimp' in usage)
+
+
+ def test_fancy_help_1(self):
+ arguments = 'foo -h'.split()
+ capture = Capture()
+ try:
+ result = commander.launch(GOPT_A, CMDTABLE_A, arguments)
+ except commander.HelpWanted, herr:
+ commander.handlehelp(herr, GOPT_A, CMDTABLE_A, output=capture)
+ usage = ''.join(capture)
+ self.assert_('number one' in usage)
+
+ def test_fancy_help_2(self):
+ arguments = '-h'.split()
+ capture = Capture()
+ try:
+ result = commander.launch(GOPT_A, CMDTABLE_A, arguments)
+ except commander.HelpWanted, herr:
+ commander.handlehelp(herr, GOPT_A, CMDTABLE_A, output=capture)
+ usage = ''.join(capture)
+ self.assert_('foo' in usage)
+ self.assert_('bar' in usage)
+ self.assert_('baz' in usage)
+
+ def test_fancy_help_3(self):
+ def tst(arguments):
+ capture = Capture()
+ try:
+ result = commander.launch(GOPT_A, CMDTABLE_A, arguments)
+ except commander.HelpWanted, herr:
+ commander.handlehelp(herr, GOPT_A, CMDTABLE_A, output=capture)
+ return ''.join(capture)
+ c1 = tst('foo --help'.split())
+ c2 = tst('foo -h'.split())
+ c3 = tst('--help foo'.split())
+ c4 = tst('help foo'.split())
+ self.assertEqual(c1, c2)
+ self.assertEqual(c2, c3)
+ self.assertEqual(c3, c4)
+
+ c1 = tst('help'.split())
+ c2 = tst('--help'.split())
+ c3 = tst('-h'.split())
+ self.assertEqual(c1, c2)
+ self.assertEqual(c2, c3)
+
+
+
+class Capture(list):
+ def write(self, x):
+ self.append(x)
+
+
if __name__ == '__main__':
unittest.main()
@@ 25,6 25,9 @@ class InvalidCommand(cli.CliError):
"""The CLI was given an invalid command"""
pass
+class MissingCommand(cli.CliError):
+ """The user failed to specify a command"""
+ pass
class AmbiguousCommand(cli.CliError):
"""The given command was ambigouous"""
@@ 57,7 60,13 @@ def launch(globalopts, cmdtable, argumen
cmdtable._table[HELP.name] = HELP
globalopts._table[HELPOPT.name] = HELPOPT
# parse cli
- cmd, opts, args = parse(globalopts, cmdtable, arguments)
+ try:
+ cmd, opts, args = parse(globalopts, cmdtable, arguments)
+ except MissingCommand:
+ cmd = HELP
+ opts, args = cli.parse(globalopts, arguments, strict=True)
+ if not opts.get('help'):
+ raise
# if help requested: raise HelpWanted
if cmd == HELP or opts.get('help'):
raise HelpWanted(cmd, opts, args)
@@ 77,12 86,64 @@ def parse(globalopts, cmdtable, argument
cmds = CommandTable(cmdtable)
opts, args = cli._parse(gopts, arguments, True)
# TODO : handle missing command
+ if not args:
+ raise MissingCommand()
cname, args = args[0], args[1:]
cmd = cmds.find(cname)
copts, cargs = cli._parse(cmd.opts + gopts, args, cmd.strict)
copts.update(opts)
return cmd, copts, cargs
-
+
+
+def handlehelp(helperr, globalopts, cmdtable, output=None, generic=None):
+ if output is None:
+ output = sys.stdout
+ if not generic:
+ generic = generic_usage
+ # determine what type of help we're getting
+ if helperr.cmd != HELP:
+ check = helperr.cmd.name
+ elif helperr.args:
+ check = helperr.args[0]
+ else:
+ check = None
+ # produce a help text iter
+ cmdtable = CommandTable(cmdtable)
+ try:
+ if check is None:
+ content = generic(cmdtable)
+ else:
+ content = usage(cmdtable.find(check))
+ except cli.CliError, err:
+ content = ['error: %s' % err]
+ for line in content:
+ output.write('%s\n' % line)
+
+
+def generic_usage(cmdtable):
+ yield 'Application Subcommands:'
+ yield ''
+ for cmd in sorted(cmdtable):
+ for line in usage(cmd, short=True):
+ yield ' %-12s %s' % (cmd.name, line)
+
+
+
+def usage(cmd, short=False, prefix=''):
+ if short:
+ yield cmd.docstring().splitlines()[0]
+ return
+ yield 'usage: %s%s [OPTIONS] %s' % (prefix, cmd.name, cmd.help)
+ doc = cmd.docstring().strip()
+ if doc:
+ yield ''
+ yield '%s' % doc
+ yield ''
+ if cmd.opts and list(cmd.opts.longopts()):
+ yield 'OPTIONS:'
+ for line in cli.usage(cmd.opts):
+ yield line
+
class Command(object):
"""A thin class representing a command.
@@ 108,6 169,15 @@ class Command(object):
else:
return []
+ def docstring(self):
+ if self.target and hasattr(self.target, '__doc__'):
+ return self.target.__doc__ or 'No usage available'
+ else:
+ return 'No usage available'
+
+ def __cmp__(self, other):
+ return cmp(self.name, other.name)
+
@classmethod
def convert(cls, obj):
if isinstance(obj, cls):