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
26 import readline # including readline makes cmd.Cmd.cmdloop() smarter
29 from ..command import CommandExit, Exit, Command, Argument, StoreValue
30 from ..interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
31 from ..ui import UserInterface, CommandMessage
32 from ..util.encoding import get_input_encoding, get_output_encoding
35 # Define a few helper classes.
37 class Default (object):
38 """Marker for options not given on the command line.
42 class CommandLineParser (optparse.OptionParser):
43 """Implement a command line syntax for a
44 :class:`hooke.command.Command`.
46 def __init__(self, command, name_fn):
47 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
48 self.command = command
49 self.command_opts = []
50 self.command_args = []
51 for a in command.arguments:
53 continue # 'help' is a default OptionParser option
54 if a.optional == True:
55 name = name_fn(a.name)
57 '--%s' % name, dest=name, default=Default)
58 self.command_opts.append(a)
60 self.command_args.append(a)
61 infinite_counters = [a for a in self.command_args if a.count == -1]
62 assert len(infinite_counters) <= 1, \
63 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
64 % (command.name, ', '.join([a.name for a in infinite_counters]))
65 if len(infinite_counters) == 1: # move the big counter to the end.
66 infinite_counter = infinite_counters[0]
67 self.command_args.remove(infinite_counter)
68 self.command_args.append(infinite_counter)
70 def exit(self, status=0, msg=None):
71 """Override :meth:`optparse.OptionParser.exit` which calls
75 raise optparse.OptParseError(msg)
76 raise optparse.OptParseError('OptParse EXIT')
78 class CommandMethod (object):
79 """Base class for method replacer.
81 The .__call__ methods of `CommandMethod` subclasses functions will
82 provide the `do_*`, `help_*`, and `complete_*` methods of
85 def __init__(self, cmd, command, name_fn):
87 self.command = command
88 self.name_fn = name_fn
90 def __call__(self, *args, **kwargs):
91 raise NotImplementedError
93 class DoCommand (CommandMethod):
94 def __init__(self, *args, **kwargs):
95 super(DoCommand, self).__init__(*args, **kwargs)
96 self.parser = CommandLineParser(self.command, self.name_fn)
98 def __call__(self, args):
100 args = self._parse_args(args)
101 except optparse.OptParseError, e:
102 self.cmd.stdout.write(str(e).lstrip()+'\n')
103 self.cmd.stdout.write('Failure\n')
105 self.cmd.inqueue.put(CommandMessage(self.command, args))
107 msg = self.cmd.outqueue.get()
108 if isinstance(msg, Exit):
110 elif isinstance(msg, CommandExit):
111 self.cmd.stdout.write(msg.__class__.__name__+'\n')
112 self.cmd.stdout.write(str(msg).rstrip()+'\n')
114 elif isinstance(msg, ReloadUserInterfaceConfig):
115 self.cmd.ui.reload_config(msg.config)
117 elif isinstance(msg, Request):
118 self._handle_request(msg)
120 self.cmd.stdout.write(str(msg).rstrip()+'\n')
122 def _parse_args(self, args):
123 options,args = self.parser.parse_args(args)
124 self._check_argument_length_bounds(args)
126 for argument in self.parser.command_opts:
127 value = getattr(options, self.name_fn(argument.name))
129 params[argument.name] = value
131 for argument in self.parser.command_args:
132 if argument.count == 1:
133 params[argument.name] = args[arg_index]
134 elif argument.count > 1:
135 params[argument.name] = \
136 args[arg_index:arg_index+argument.count]
137 else: # argument.count == -1:
138 params[argument.name] = args[arg_index:]
139 arg_index += argument.count
142 def _check_argument_length_bounds(self, arguments):
143 """Check that there are an appropriate number of arguments in
146 If not, raise optparse.OptParseError().
150 for argument in self.parser.command_args:
151 if argument.optional == False and argument.count > 0:
152 min_args += argument.count
153 if max_args >= 0: # otherwise already infinite
154 if argument.count == -1:
157 max_args += argument.count
158 if len(arguments) < min_args \
159 or (max_args >= 0 and len(arguments) > max_args):
160 if min_args == max_args:
161 target_string = str(min_args)
163 target_string = 'more than %d' % min_args
165 target_string = '%d to %d' % (min_args, max_args)
166 raise optparse.OptParseError(
167 '%d arguments given, but %s takes %s'
168 % (len(arguments), self.name_fn(self.command.name),
171 def _handle_request(self, msg):
172 """Repeatedly try to get a response to `msg`.
174 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
176 raise NotImplementedError('_%s_request_prompt' % msg.type)
177 prompt_string = prompt(msg)
178 parser = getattr(self, '_%s_request_parser' % msg.type, None)
180 raise NotImplementedError('_%s_request_parser' % msg.type)
184 self.cmd.stdout.write(''.join([
185 error.__class__.__name__, ': ', str(error), '\n']))
186 self.cmd.stdout.write(prompt_string)
187 value = parser(msg, self.cmd.stdin.readline())
189 response = msg.response(value)
191 except ValueError, error:
193 self.cmd.inqueue.put(response)
195 def _boolean_request_prompt(self, msg):
196 if msg.default == True:
202 def _boolean_request_parser(self, msg, response):
203 value = response.strip().lower()
204 if value.startswith('y'):
206 elif value.startswith('n'):
208 elif len(value) == 0:
212 def _string_request_prompt(self, msg):
213 if msg.default == None:
216 d = ' [%s] ' % msg.default
219 def _string_request_parser(self, msg, response):
220 return response.strip()
222 def _float_request_prompt(self, msg):
223 return self._string_request_prompt(msg)
225 def _float_request_parser(self, msg, resposne):
226 return float(response)
228 def _selection_request_prompt(self, msg):
230 for i,option in enumerate(msg.options):
231 options.append(' %d) %s' % (i,option))
232 options = ''.join(options)
233 if msg.default == None:
236 prompt = '? [%d] ' % msg.default
237 return '\n'.join([msg,options,prompt])
239 def _selection_request_parser(self, msg, response):
243 class HelpCommand (CommandMethod):
244 def __init__(self, *args, **kwargs):
245 super(HelpCommand, self).__init__(*args, **kwargs)
246 self.parser = CommandLineParser(self.command, self.name_fn)
249 blocks = [self.command.help(name_fn=self.name_fn),
251 'Usage: ' + self._usage_string(),
253 self.cmd.stdout.write('\n'.join(blocks))
256 return self.command.help(name_fn=self.name_fn)
258 def _usage_string(self):
259 if len(self.parser.command_opts) == 0:
262 options_string = '[options]'
263 arg_string = ' '.join(
264 [self.name_fn(arg.name) for arg in self.parser.command_args])
265 return ' '.join([x for x in [self.parser.prog,
270 class CompleteCommand (CommandMethod):
271 def __call__(self, text, line, begidx, endidx):
275 # Define some additional commands
277 class LocalHelpCommand (Command):
278 """Called with an argument, prints that command's documentation.
280 With no argument, lists all available help topics as well as any
281 undocumented commands.
284 super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
285 # We set .arguments now (vs. using th arguments option to __init__),
286 # to overwrite the default help argument. We don't override
287 # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
289 Argument(name='command', type='string', optional=True,
290 help='The name of the command you want help with.')
293 def _run(self, hooke, inqueue, outqueue, params):
294 raise NotImplementedError # cmd.Cmd already implements .do_help()
296 class LocalExitCommand (Command):
297 """Exit Hooke cleanly.
300 super(LocalExitCommand, self).__init__(
301 name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
303 Argument(name='force', type='bool', default=False,
305 Exit without prompting the user. Use if you save often or don't make
310 def _run(self, hooke, inqueue, outqueue, params):
311 """The guts of the `do_exit/_quit/_EOF` commands.
313 A `True` return stops :meth:`.cmdloop` execution.
316 if params['force'] == False:
317 not_saved = [p.name for p in hooke.playlists
318 if p.is_saved() == False]
321 if len(not_saved) > 0:
322 msg = 'Unsaved playlists (%s). %s' \
323 % (', '.join([str(p) for p in not_saved]), msg)
325 outqueue.put(BooleanRequest(msg, default))
326 result = inqueue.get()
327 assert result.type == 'boolean'
333 # Now onto the main attraction.
335 class HookeCmd (cmd.Cmd):
336 def __init__(self, ui, commands, inqueue, outqueue):
337 cmd.Cmd.__init__(self)
339 self.commands = commands
340 self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
341 self.prompt = 'hooke> '
342 self._add_command_methods()
343 self.inqueue = inqueue
344 self.outqueue = outqueue
346 def _name_fn(self, name):
347 return name.replace(' ', '_')
349 def _add_command_methods(self):
350 for command in self.commands + self.local_commands:
351 for name in [command.name] + command.aliases:
352 name = self._name_fn(name)
353 setattr(self.__class__, 'help_%s' % name,
354 HelpCommand(self, command, self._name_fn))
356 setattr(self.__class__, 'do_%s' % name,
357 DoCommand(self, command, self._name_fn))
358 setattr(self.__class__, 'complete_%s' % name,
359 CompleteCommand(self, command, self._name_fn))
361 def parseline(self, line):
362 """Override Cmd.parseline to use shlex.split.
366 This allows us to handle comments cleanly. With the default
367 Cmd implementation, a pure comment line will call the .default
370 Since we use shlex to strip comments, we return a list of
371 split arguments rather than the raw argument string.
374 argv = shlex.split(line, comments=True, posix=True)
376 return None, None, '' # return an empty line
381 return argv[0], argv[1:], line
383 def do_help(self, arg):
384 """Wrap Cmd.do_help to handle our .parseline argument list.
387 return cmd.Cmd.do_help(self, '')
388 return cmd.Cmd.do_help(self, arg[0])
391 """Override Cmd.emptyline to not do anything.
393 Repeating the last non-empty command seems unwise. Explicit
394 is better than implicit.
398 class CommandLine (UserInterface):
399 """Command line interface. Simple and powerful.
402 super(CommandLine, self).__init__(name='command line')
404 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
405 cmd = HookeCmd(self, commands,
406 inqueue=ui_to_command_queue,
407 outqueue=command_to_ui_queue)
408 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
409 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
412 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
413 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
414 cmd.cmdloop(self._splash_text())
416 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
418 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)