1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This file is part of Hooke.
5 # Hooke is free software: you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation, either
8 # version 3 of the License, or (at your option) any later version.
10 # Hooke is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Lesser General Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke. If not, see
17 # <http://www.gnu.org/licenses/>.
19 """Defines :class:`CommandLine` for driving Hooke from the command
25 import readline # including readline makes cmd.Cmd.cmdloop() smarter
28 from ..command import CommandExit, Exit, Command, Argument, StoreValue
29 from ..interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
30 from ..ui import UserInterface, CommandMessage
33 # Define a few helper classes.
35 class Default (object):
36 """Marker for options not given on the command line.
40 class CommandLineParser (optparse.OptionParser):
41 """Implement a command line syntax for a
42 :class:`hooke.command.Command`.
44 def __init__(self, command, name_fn):
45 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
46 self.command = command
47 self.command_opts = []
48 self.command_args = []
49 for a in command.arguments:
51 continue # 'help' is a default OptionParser option
52 if a.optional == True:
53 name = name_fn(a.name)
55 '--%s' % name, dest=name, default=Default)
56 self.command_opts.append(a)
58 self.command_args.append(a)
59 infinite_counters = [a for a in self.command_args if a.count == -1]
60 assert len(infinite_counters) <= 1, \
61 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
62 % (command.name, ', '.join([a.name for a in infinite_counters]))
63 if len(infinite_counters) == 1: # move the big counter to the end.
64 infinite_counter = infinite_counters[0]
65 self.command_args.remove(infinite_counter)
66 self.command_args.append(infinite_counter)
68 def exit(self, status=0, msg=None):
69 """Override :meth:`optparse.OptionParser.exit` which calls
73 raise optparse.OptParseError(msg)
74 raise optparse.OptParseError('OptParse EXIT')
76 class CommandMethod (object):
77 """Base class for method replacer.
79 The .__call__ methods of `CommandMethod` subclasses functions will
80 provide the `do_*`, `help_*`, and `complete_*` methods of
83 def __init__(self, cmd, command, name_fn):
85 self.command = command
86 self.name_fn = name_fn
88 def __call__(self, *args, **kwargs):
89 raise NotImplementedError
91 class DoCommand (CommandMethod):
92 def __init__(self, *args, **kwargs):
93 super(DoCommand, self).__init__(*args, **kwargs)
94 self.parser = CommandLineParser(self.command, self.name_fn)
96 def __call__(self, args):
98 args = self._parse_args(args)
99 except optparse.OptParseError, e:
100 self.cmd.stdout.write(str(e).lstrip()+'\n')
101 self.cmd.stdout.write('Failure\n')
103 self.cmd.inqueue.put(CommandMessage(self.command, args))
105 msg = self.cmd.outqueue.get()
106 if isinstance(msg, Exit):
108 elif isinstance(msg, CommandExit):
109 self.cmd.stdout.write(msg.__class__.__name__+'\n')
110 self.cmd.stdout.write(str(msg).rstrip()+'\n')
112 elif isinstance(msg, ReloadUserInterfaceConfig):
113 self.cmd.ui.reload_config(msg.config)
115 elif isinstance(msg, Request):
116 self._handle_request(msg)
118 self.cmd.stdout.write(str(msg).rstrip()+'\n')
120 def _parse_args(self, args):
121 argv = shlex.split(args, comments=True, posix=True)
122 options,args = self.parser.parse_args(argv)
123 self._check_argument_length_bounds(args)
125 for argument in self.parser.command_opts:
126 value = getattr(options, self.name_fn(argument.name))
128 params[argument.name] = value
130 for argument in self.parser.command_args:
131 if argument.count == 1:
132 params[argument.name] = args[arg_index]
133 elif argument.count > 1:
134 params[argument.name] = \
135 args[arg_index:arg_index+argument.count]
136 else: # argument.count == -1:
137 params[argument.name] = args[arg_index:]
138 arg_index += argument.count
141 def _check_argument_length_bounds(self, arguments):
142 """Check that there are an appropriate number of arguments in
145 If not, raise optparse.OptParseError().
149 for argument in self.parser.command_args:
150 if argument.optional == False and argument.count > 0:
151 min_args += argument.count
152 if max_args >= 0: # otherwise already infinite
153 if argument.count == -1:
156 max_args += argument.count
157 if len(arguments) < min_args \
158 or (max_args >= 0 and len(arguments) > max_args):
159 if min_args == max_args:
160 target_string = str(min_args)
162 target_string = 'more than %d' % min_args
164 target_string = '%d to %d' % (min_args, max_args)
165 raise optparse.OptParseError(
166 '%d arguments given, but %s takes %s'
167 % (len(arguments), self.name_fn(self.command.name),
170 def _handle_request(self, msg):
171 """Repeatedly try to get a response to `msg`.
173 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
175 raise NotImplementedError('_%s_request_prompt' % msg.type)
176 prompt_string = prompt(msg)
177 parser = getattr(self, '_%s_request_parser' % msg.type, None)
179 raise NotImplementedError('_%s_request_parser' % msg.type)
183 self.cmd.stdout.write(''.join([
184 error.__class__.__name__, ': ', str(error), '\n']))
185 self.cmd.stdout.write(prompt_string)
186 value = parser(msg, self.cmd.stdin.readline())
188 response = msg.response(value)
190 except ValueError, error:
192 self.cmd.inqueue.put(response)
194 def _boolean_request_prompt(self, msg):
195 if msg.default == True:
201 def _boolean_request_parser(self, msg, response):
202 value = response.strip().lower()
203 if value.startswith('y'):
205 elif value.startswith('n'):
207 elif len(value) == 0:
211 def _string_request_prompt(self, msg):
212 if msg.default == None:
215 d = ' [%s] ' % msg.default
218 def _string_request_parser(self, msg, response):
219 return response.strip()
221 def _float_request_prompt(self, msg):
222 return self._string_request_prompt(msg)
224 def _float_request_parser(self, msg, resposne):
225 return float(response)
227 def _selection_request_prompt(self, msg):
229 for i,option in enumerate(msg.options):
230 options.append(' %d) %s' % (i,option))
231 options = ''.join(options)
232 if msg.default == None:
235 prompt = '? [%d] ' % msg.default
236 return '\n'.join([msg,options,prompt])
238 def _selection_request_parser(self, msg, response):
242 class HelpCommand (CommandMethod):
243 def __init__(self, *args, **kwargs):
244 super(HelpCommand, self).__init__(*args, **kwargs)
245 self.parser = CommandLineParser(self.command, self.name_fn)
248 blocks = [self.command.help(name_fn=self.name_fn),
250 'Usage: ' + self._usage_string(),
252 self.cmd.stdout.write('\n'.join(blocks))
255 return self.command.help(name_fn=self.name_fn)
257 def _usage_string(self):
258 if len(self.parser.command_opts) == 0:
261 options_string = '[options]'
262 arg_string = ' '.join(
263 [self.name_fn(arg.name) for arg in self.parser.command_args])
264 return ' '.join([x for x in [self.parser.prog,
269 class CompleteCommand (CommandMethod):
270 def __call__(self, text, line, begidx, endidx):
274 # Define some additional commands
276 class LocalHelpCommand (Command):
277 """Called with an argument, prints that command's documentation.
279 With no argument, lists all available help topics as well as any
280 undocumented commands.
283 super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
284 # We set .arguments now (vs. using th arguments option to __init__),
285 # to overwrite the default help argument. We don't override
286 # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
288 Argument(name='command', type='string', optional=True,
289 help='The name of the command you want help with.')
292 def _run(self, hooke, inqueue, outqueue, params):
293 raise NotImplementedError # cmd.Cmd already implements .do_help()
295 class LocalExitCommand (Command):
296 """Exit Hooke cleanly.
299 super(LocalExitCommand, self).__init__(
300 name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
302 Argument(name='force', type='bool', default=False,
304 Exit without prompting the user. Use if you save often or don't make
309 def _run(self, hooke, inqueue, outqueue, params):
310 """The guts of the `do_exit/_quit/_EOF` commands.
312 A `True` return stops :meth:`.cmdloop` execution.
315 if params['force'] == False:
316 not_saved = [p.name for p in hooke.playlists
317 if p.is_saved() == False]
320 if len(not_saved) > 0:
321 msg = 'Unsaved playlists (%s). %s' \
322 % (', '.join([str(p) for p in not_saved]), msg)
324 outqueue.put(BooleanRequest(msg, default))
325 result = inqueue.get()
326 assert result.type == 'boolean'
332 # Now onto the main attraction.
334 class HookeCmd (cmd.Cmd):
335 def __init__(self, ui, commands, inqueue, outqueue):
336 cmd.Cmd.__init__(self)
338 self.commands = commands
339 self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
340 self.prompt = 'hooke> '
341 self._add_command_methods()
342 self.inqueue = inqueue
343 self.outqueue = outqueue
345 def _name_fn(self, name):
346 return name.replace(' ', '_')
348 def _add_command_methods(self):
349 for command in self.commands + self.local_commands:
350 for name in [command.name] + command.aliases:
351 name = self._name_fn(name)
352 setattr(self.__class__, 'help_%s' % name,
353 HelpCommand(self, command, self._name_fn))
355 setattr(self.__class__, 'do_%s' % name,
356 DoCommand(self, command, self._name_fn))
357 setattr(self.__class__, 'complete_%s' % name,
358 CompleteCommand(self, command, self._name_fn))
361 class CommandLine (UserInterface):
362 """Command line interface. Simple and powerful.
365 super(CommandLine, self).__init__(name='command line')
367 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
368 cmd = HookeCmd(self, commands,
369 inqueue=ui_to_command_queue,
370 outqueue=command_to_ui_queue)
371 cmd.cmdloop(self._splash_text())
373 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
375 cmd = HookeCmd(self, commands,
376 inqueue=ui_to_command_queue,
377 outqueue=command_to_ui_queue)