@@ 1,248 1,63 @@
import unittest
-from vanity import cli
-
-
-SAMPLE1 = '--output foo.txt bar.txt'.split()
-SAMPLE2 = 'bar.txt -o foo.txt -e --verbose'.split()
-
-TABLE_A = [
- ('output', 'o', '', 'Output file'),
- ('extra', 'e', None, 'Add extra markers'),
- ('verbose', '', None, 'Be verbose'),
- ]
-
-# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+from vanity import commander
-OPTIONS1 = [
- ('template', 't', '', 'Set template file'),
- ('output', 'o', '', 'Output file'),
- ('format', '', '', 'Input/Output format'),
- ('check', 'c', None, 'Validate input before processing'),
- ('verbose', '', None, 'Be verbose'),
+GOPT_A = [
+ ('verbose', 'v', None, 'be verbose'),
+ ('quiet', 'q', None, 'be quiet'),
]
-CLI1 = 'a.txt b.txt'
-CLI2 = '-o out.txt a.txt'
-CLI3 = '--verbose -t foo.x -o out.txt b.txt'
-CLI4 = '--bad --verbose c.txt'
-CLI5 = '-c a.txt -o moose.txt'
-
-OPTIONS2 = [
- ('run-tests', '', None, 'Run tests'),
- ('skip-checks', '', None, 'Skip detailed checks'),
- ('set-modules', '', '.', 'Location for modules'),
- ('set-destination', 'd', '.', 'Destination directory'),
- ]
-
-CLI6 = ''
-CLI7 = '--run-tests --skip-checks --set-modules /tmp --set-destination=/tmp'
-CLI8 = '-d /home/foo -q'
-CLI9 = '--set-destination --run-tests'
-
def cmd1():
- pass
-def cmd2():
- pass
-def cmd3():
- pass
+ return 1
-OPTIONS3 = [
- ('debug', 'D', None, 'Enable debugging'),
- ('help', '?', None, 'Show help'),
- ]
-COMMANDS1 = [
- (cmd1, 'draw', OPTIONS2, 'Draw on slate'),
- (cmd2, 'wipe', [], 'Wipe slate'),
- (cmd3, 'window', [], 'Open a new window'),
- (cmd3, 'fit', [], 'rezise objects to fit on slate'),
- (cmd3, 'fitness', [], 'check if all objects fit'),
- ]
+def cmd2():
+ return 2
-CLI10 = 'draw fribble.s'
-CLI11 = '--debug draw fribble.s'
-CLI12 = 'wipe --debug fribble.s'
-CLI13 = 'dr --help -d /home/blarg --skip-checks fribble.s'
-CLI14 = '--help --skip-checks dr -d . fribble.s'
-CLI15 = '--help'
-CLI16 = "fit womp.s"
-CLI17 = "fi womp.s"
-
+def cmd3():
+ return 3
-# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
-
-"""
-class XXXTestCli(unittest.TestCase):
- def test_cli_1(self):
- opts, args = cli.parse(OPTIONS1, CLI1.split())
- self.assertTrue(args == ['a.txt', 'b.txt'])
- self.assertTrue(opts['template'] == '')
- self.assertTrue(opts['output'] == '')
- self.assertTrue(opts['format'] == '')
- self.assertFalse(opts['check'])
- self.assertFalse(opts['verbose'])
-
- def test_cli_2(self):
- opts, args = cli.parse(OPTIONS1, CLI2.split())
- self.assertTrue(args == ['a.txt'])
- self.assertTrue(opts['template'] == '')
- self.assertTrue(opts['output'] == 'out.txt')
- self.assertTrue(opts['format'] == '')
- self.assertFalse(opts['check'])
- self.assertFalse(opts['verbose'])
-
- def test_cli_3(self):
- opts, args = cli.parse(OPTIONS1, CLI3.split())
- self.assertTrue(args == ['b.txt'])
- self.assertTrue(opts['template'] == 'foo.x')
- self.assertTrue(opts['output'] == 'out.txt')
- self.assertTrue(opts['format'] == '')
- self.assertFalse(opts['check'])
- self.assertTrue(opts['verbose'])
-
- def test_cli_4(self):
- self.assertRaises(
- cli.InvalidOption,
- cli.parse,
- OPTIONS1, CLI4.split()
- )
+CMDTABLE_A = [
+ ('foo', cmd1, '', [], 'foo fixes everything'),
+ ('bar', cmd2, '', [], 'bar breaks it'),
+ ('baz', cmd3, '', [
+ ('shrimp', '', None, 'jumbo shrimp'),
+ ], 'something helpful'),
+ ]
- def test_cli_5(self):
- opts, args = cli.parse(OPTIONS1, CLI5.split())
- self.assertTrue(args == ['a.txt'])
- self.assertTrue(opts['template'] == '')
- self.assertTrue(opts['output'] == 'moose.txt')
- self.assertTrue(opts['format'] == '')
- self.assertTrue(opts['check'])
- self.assertFalse(opts['verbose'])
-
- def test_cli_6(self):
- opts, args = cli.parse(OPTIONS2, CLI6.split())
- self.assertTrue(args == [])
- self.assertFalse(opts.get('run-tests'))
- self.assertFalse(opts.get('skip-checks'))
- self.assertTrue(opts.get('set-modules') == '.')
- self.assertTrue(opts.get('set-destination') == '.')
+class TestCliCommands(unittest.TestCase):
- def test_cli_7(self):
- opts, args = cli.parse(OPTIONS2, CLI7.split())
- self.assertTrue(args == [])
- self.assertTrue(opts.get('run-tests'))
- self.assertTrue(opts.get('skip-checks'))
- self.assertTrue(opts.get('set-modules') == '/tmp')
- self.assertTrue(opts.get('set-destination') == '/tmp')
-
- def test_cli_8(self):
- self.assertRaises(
- cli.InvalidOption,
- cli.parse,
- OPTIONS2, CLI8.split()
- )
-
- def test_cli_9(self):
- opts, args = cli.parse(OPTIONS2, CLI9.split())
- self.assertTrue(args == [])
- self.assertFalse(opts.get('run-tests'))
- self.assertFalse(opts.get('skip-checks'))
- self.assertTrue(opts.get('set-modules') == '.')
- # a screwed up command line
- self.assertTrue(opts.get('set-destination') == '--run-tests')
+ def test_parse_cli_cmd(self):
+ arguments = 'foo test.txt'.split()
+ cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+ self.assertEqual(cmd.target, cmd1)
+ self.assertEqual(args, ['test.txt'])
- def test_cli_10(self):
- fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI10.split())
- self.assertTrue(fn == cmd1)
- self.assertTrue('debug' in opts)
- self.assertFalse(opts['debug'])
- self.assertTrue('help' in opts)
- self.assertFalse(opts['help'])
- self.assertTrue('run-tests' in opts)
-
- def test_cli_11(self):
- fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI11.split())
- self.assertTrue(fn == cmd1)
- self.assertTrue('debug' in opts)
- self.assertTrue(opts['debug'])
- self.assertTrue('help' in opts)
- self.assertFalse(opts['help'])
- self.assertTrue('run-tests' in opts)
-
- def test_cli_12(self):
- fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI12.split())
- self.assertTrue(fn == cmd2)
- self.assertTrue('debug' in opts)
- self.assertTrue(opts['debug'])
- self.assertTrue('help' in opts)
- self.assertFalse(opts['help'])
- self.assertFalse('run-tests' in opts)
-
- def test_cli_13(self):
- fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI13.split())
- self.assertTrue(fn == cmd1)
- self.assertTrue('debug' in opts)
- self.assertFalse(opts['debug'])
- self.assertTrue('help' in opts)
- self.assertTrue(opts['help'])
- self.assertTrue('run-tests' in opts)
-
- def test_cli_14(self):
- self.assertRaises(
- cli.InvalidOption,
- cli.parsecommand,
- OPTIONS3, COMMANDS1, CLI14.split()
- )
+ def test_parse_cli_cmd2(self):
+ arguments = 'foo -v test.txt'.split()
+ cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+ self.assertEqual(cmd.target, cmd1)
+ self.assertEqual(args, ['test.txt'])
+ self.assertEqual(opts, dict(verbose=True, quiet=None))
- def test_cli_15(self):
- fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI15.split())
- self.assertTrue(fn == None)
- self.assertTrue(args == [])
- self.assertTrue('debug' in opts)
- self.assertTrue('help' in opts)
- self.assertFalse(opts['debug'])
- self.assertTrue(opts['help'])
-
- def test_cli_16(self):
- fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI16.split())
- self.assertTrue(fn == cmd3)
-
- def test_cli_17(self):
- self.assertRaises(
- cli.AmbiguousCommand,
- cli.parsecommand,
- OPTIONS3, COMMANDS1, CLI17.split()
- )
-
+ def test_parse_cli_cmd3(self):
+ arguments = '-v foo test.txt --quiet'.split()
+ cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+ self.assertEqual(cmd.target, cmd1)
+ self.assertEqual(args, ['test.txt'])
+ self.assertEqual(opts, dict(verbose=True, quiet=True))
- def test_parse_sample_1(self):
- opts, args = cli.parse(TABLE_A, SAMPLE1)
- self.assertTrue('output' in opts)
- self.assertTrue('extra' in opts)
- self.assertTrue('verbose' in opts)
- self.assertTrue(opts['output'] == 'foo.txt')
- self.assertFalse(opts['extra'])
- self.assertFalse(opts['verbose'])
- self.assertTrue(args == ['bar.txt'])
-
- def test_parse_sample_2(self):
- opts, args = cli.parse(TABLE_A, SAMPLE2)
- self.assertTrue('output' in opts)
- self.assertTrue('extra' in opts)
- self.assertTrue('verbose' in opts)
- self.assertTrue(opts['output'] == 'foo.txt')
- self.assertTrue(opts['extra'])
- self.assertTrue(opts['verbose'])
- self.assertTrue(args == ['bar.txt'])
-
- def test_usage(self):
- ug = list(cli.usage(TABLE_A))
- #print '\n%s\n' % '\n'.join(ug)
- self.assertTrue(len(ug) == 3)
-
-"""
-
-
+ def test_parse_cli_cmd4(self):
+ arguments = '-v baz test.txt --shrimp'.split()
+ cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+ self.assertEqual(cmd.target, cmd3)
+ self.assertEqual(args, ['test.txt'])
+ self.assertEqual(opts, dict(verbose=True, quiet=None, shrimp=True))
+ arguments = '-v baz test.txt'.split()
+ cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+ self.assertEqual(opts, dict(verbose=True, quiet=None, shrimp=None))
if __name__ == '__main__':
@@ 0,0 1,274 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 John Mulligan <phlogistonjohn@asynchrono.us>
+#
+# This software may be used and distributed according to the terms of the
+# MIT license, incorporated herein by reference. A copy of this license
+# should accompany the source code in a file named COPYING.txt.
+#
+"""parse command lines with subcommands
+
+
+XXX
+ret = commander.launch(globalopts, cmdtable, args)
+
+cmd, opts, args = commander.parse(globalopts, cmdtable, args)
+
+XXX
+"""
+
+from vanity import cli
+
+
+class InvalidCommand(cli.CliError):
+ """The CLI was given an invalid command"""
+ pass
+
+
+class AmbiguousCommand(cli.CliError):
+ """The given command was ambigouous"""
+ def __init__(self, matches):
+ msg = 'possible matches: %s' % ' '.join(matches)
+ CliError.__init__(self, msg)
+ self.matches = matches
+
+
+def launch(globalopts, cmdtable, arguments):
+ """Launch a subcommand based on the given arguments, return the
+ result of the launched function.
+
+ * ``globalopts`` - an options table common to all sub commands
+ * ``cmdtable`` - a commands table
+ * ``arguments`` - the command line arguments
+ """
+ pass
+
+
+def parse(globalopts, cmdtable, arguments):
+ """Parse a cli and return the corresponding command, parsed options
+ and arguments.
+
+ * ``globalopts`` - an options table common to all sub commands
+ * ``cmdtable`` - a commands table
+ * ``arguments`` - the command line arguments
+ """
+ gopts = EarlyOptionTable(globalopts)
+ cmds = CommandTable(cmdtable)
+ opts, args = cli._parse(gopts, arguments, True)
+ # TODO : handle missing command
+ 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
+
+
+class Command(object):
+ """A thin class representing a command.
+
+ Attributes: name, target, aliases, help, strict
+ """
+
+ def __init__(self, name, target, aliases=None, opts=None,
+ help=None, strict=None):
+ self.name = name
+ self.target = target
+ self.aliases = aliases
+ self.help = help
+ self.opts = cli.OptionTable(opts)
+ self.strict = strict
+
+ def __call__(self, opts, args):
+ return self.target(*args, **opts)
+
+ def aliaslist(self):
+ if self.aliases:
+ return self.aliases.split('|')
+ else:
+ return []
+
+ @classmethod
+ def convert(cls, obj):
+ if isinstance(obj, cls):
+ return obj
+ if hasattr(obj, 'keys'):
+ return cls(**obj)
+ return cls(*obj)
+
+
+class EarlyOptionTable(cli.OptionTable):
+ """Special case options table for before-command opts"""
+
+ def assemble(self, opts):
+ """an assemble function that will not return all keys"""
+ assembled = cli.OptionTable.assemble(self, opts)
+ return dict((name, assembled[name]) for name in self._names(opts))
+
+ def _names(self, opts):
+ for key, _ in opts:
+ try:
+ yield self.getlong(key).name
+ except KeyError:
+ yield self.getshort(key).name
+
+
+class CommandTable(object):
+ """A table of launchable sub-commands.
+ """
+
+ def __init__(self, table):
+ self._table = {}
+ if hasattr(table, 'keys'):
+ # if a user is giving us a dict, the entries **must** be dicts
+ for name, entry in table:
+ self._table[name] = Command(name, **entry)
+ else:
+ for entry in table:
+ cmd = Command.convert(entry)
+ self._table[cmd.name] = cmd
+ return
+
+ def __iter__(self):
+ return self._table.itervalues()
+
+ def find(self, name):
+ """Find the command entry that best matches the given name.
+ Returns a command, if no matches are possible an InvalidCommand
+ exception is raised, if multiple matches are possible an
+ AmbiguousCommand exception is raised.
+ """
+ namemap = dict((k,k) for k in self._table)
+ for key in self._table:
+ for alias in self._table[key].aliaslist():
+ namemap[alias] = name
+ if name in namemap:
+ key = namemap[name]
+ return self._table[key]
+ # search partials
+ canidates = set()
+ for alias in namemap:
+ if alias.startswith(name):
+ canidates.add(alias)
+ if len(canidates) == 1:
+ key = namemap[canidates[0]]
+ return self._table[key]
+ if not canidates:
+ raise InvalidCommand(name)
+ else:
+ raise AmbiguousCommand(canidates)
+
+ # TODO: "add" decorator
+
+
+
+'''
+
+
+def splitidents(idents):
+ """Return a ident string as a list of idents.
+ """
+ return str(idents).split()
+
+class CommandTable(object):
+ """Processes a table of commands and the global options.
+ """
+ def __init__(self, globalopts, commands):
+ self._global = OptionTable(globalopts)
+ self._table = {}
+ for command in commands:
+ self._addcommand(command)
+
+ def _addcommand(self, command):
+ func, idents, opts, desc = command
+ for ident in splitidents(idents):
+ self._table[ident] = (
+ func,
+ idents,
+ OptionTable(opts),
+ desc)
+
+ def labels(self):
+ return self._table.keys()
+
+
+ def firstlabels(self):
+ seen = []
+ for key in self.labels():
+ f, idents, opts, desc = self._table[key]
+ idents = splitidents(idents)
+ if idents[0] not in seen:
+ seen.append(idents[0])
+ return seen
+
+
+ def opts(self):
+ """Return the global options table
+ """
+ return self._global
+
+ def find(self, label):
+ """Given a command name or partial command name,
+ return the closest full command name.
+ If there are no matches a InvalidCommand exception is
+ raised. If there are too many matches an AmbiguousCommand
+ exception is raised.
+ """
+ matches = []
+ for key in self._table.keys():
+ if key.startswith(label):
+ matches.append(key)
+ if len(matches) == 1:
+ return matches[0]
+ if not matches:
+ raise InvalidCommand(label)
+ if label in self._table.keys():
+ #exact match
+ return label
+ raise AmbiguousCommand(label, matches)
+
+ def lookup(self, label):
+ """Return the table entry given a full or partial
+ command name.
+ """
+ key = self.find(label)
+ return self._table[key]
+
+ def get(self, label):
+ """For a full or partial command name, return the
+ function, it's identifiers, an options table containing
+ both command and global options and the command
+ description.
+ """
+ (func, id, options, desc) = self.lookup(label)
+ options = self._global + options
+ return (func, id, options, desc)
+
+
+def parsecommand(opttable, cmdtable, arguments, getopts=None):
+ """Parse a command line into an command function, an
+ options dict and an arguments list.
+ """
+ if getopts is None:
+ getopts = (_getopt.getopt, _getopt.gnu_getopt)
+ cmds = CommandTable(opttable, cmdtable)
+ short, long = (cmds.opts().shortoptspec(), cmds.opts().longoptspec())
+ try:
+ raw, args = getopts[0](arguments, short, long)
+ except _getopt.GetoptError, ee:
+ raise InvalidOption(str(ee))
+ if not args:
+ return (None, cmds.opts().assemble(raw), [])
+ label, args = args[0], args[1:]
+ (func, id, opts, desc) = cmds.get(label)
+ raw2, args = getopts[1](args, opts.shortoptspec(), opts.longoptspec())
+ return (func, opts.assemble(raw+raw2), args)
+
+
+def listcommands(cmdtable):
+ cmds = CommandTable([], cmdtable)
+ disp = list(cmds.firstlabels())
+ disp.sort()
+ for label in disp:
+ desc = cmds.get(label)[3]
+ yield (label, desc)
+'''