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 modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
13 # 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
28 import readline # including readline makes cmd.Cmd.cmdloop() smarter
29 except ImportError, e:
31 logging.warn('Could not import readline, bash-like line editing disabled.')
34 from ..command import CommandExit, Exit, Command, Argument, StoreValue
35 from ..engine import CommandMessage
36 from ..interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
37 from ..ui import UserInterface
38 from ..util.convert import from_string
39 from ..util.encoding import get_input_encoding, get_output_encoding
42 # Define a few helper classes.
44 class Default (object):
45 """Marker for options not given on the command line.
49 class CommandLineParser (optparse.OptionParser):
50 """Implement a command line syntax for a
51 :class:`hooke.command.Command`.
53 def __init__(self, command, name_fn):
54 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
55 self.command = command
56 self.command_opts = []
57 self.command_args = []
58 for a in command.arguments:
60 continue # 'help' is a default OptionParser option
61 if a.optional == True:
62 name = name_fn(a.name)
67 '--disable-%s' % name, dest=name, default=Default,
69 self.command_opts.append(a)
71 elif a.default == False:
73 '--enable-%s' % name, dest=name, default=Default,
75 self.command_opts.append(a)
79 elif type not in ['string', 'int', 'long', 'choice', 'float',
83 '--%s' % name, dest=name, type=type, default=Default)
84 self.command_opts.append(a)
86 self.command_args.append(a)
87 infinite_counters = [a for a in self.command_args if a.count == -1]
88 assert len(infinite_counters) <= 1, \
89 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
90 % (command.name, ', '.join([a.name for a in infinite_counters]))
91 if len(infinite_counters) == 1: # move the big counter to the end.
92 infinite_counter = infinite_counters[0]
93 self.command_args.remove(infinite_counter)
94 self.command_args.append(infinite_counter)
96 def exit(self, status=0, msg=None):
97 """Override :meth:`optparse.OptionParser.exit` which calls
101 raise optparse.OptParseError(msg)
102 raise optparse.OptParseError('OptParse EXIT')
104 class CommandMethod (object):
105 """Base class for method replacer.
107 The .__call__ methods of `CommandMethod` subclasses functions will
108 provide the `do_*`, `help_*`, and `complete_*` methods of
111 def __init__(self, cmd, command, name_fn):
113 self.command = command
114 self.name_fn = name_fn
116 def __call__(self, *args, **kwargs):
117 raise NotImplementedError
119 class DoCommand (CommandMethod):
120 def __init__(self, *args, **kwargs):
121 super(DoCommand, self).__init__(*args, **kwargs)
122 self.parser = CommandLineParser(self.command, self.name_fn)
123 self.log = logging.getLogger('hooke')
125 def __call__(self, args):
127 args = self._parse_args(args)
128 except optparse.OptParseError, e:
129 self.cmd.stdout.write(str(e).lstrip()+'\n')
130 self.cmd.stdout.write('Failure\n')
132 self.log.debug('executing %s with %s' % (self.command.name, args))
133 self.cmd.inqueue.put(CommandMessage(self.command.name, args))
135 msg = self.cmd.outqueue.get()
136 if isinstance(msg, Exit):
138 elif isinstance(msg, CommandExit):
139 self.cmd.stdout.write(msg.__class__.__name__+'\n')
140 self.cmd.stdout.write(str(msg).rstrip()+'\n')
142 elif isinstance(msg, ReloadUserInterfaceConfig):
143 self.cmd.ui.reload_config(msg.config)
145 elif isinstance(msg, Request):
146 self._handle_request(msg)
148 self.cmd.stdout.write(str(msg).rstrip()+'\n')
150 def _parse_args(self, args):
151 options,args = self.parser.parse_args(args)
152 self._check_argument_length_bounds(args)
154 for argument in self.parser.command_opts:
155 value = getattr(options, self.name_fn(argument.name))
157 params[argument.name] = value
159 for argument in self.parser.command_args:
160 if argument.count == 1:
161 params[argument.name] = from_string(args[arg_index],
163 elif argument.count > 1:
164 params[argument.name] = [
165 from_string(a, argument.type)
166 for a in args[arg_index:arg_index+argument.count]]
167 else: # argument.count == -1:
168 params[argument.name] = [
169 from_string(a, argument.type) for a in args[arg_index:]]
170 arg_index += argument.count
173 def _check_argument_length_bounds(self, arguments):
174 """Check that there are an appropriate number of arguments in
177 If not, raise optparse.OptParseError().
181 for argument in self.parser.command_args:
182 if argument.optional == False and argument.count > 0:
183 min_args += argument.count
184 if max_args >= 0: # otherwise already infinite
185 if argument.count == -1:
188 max_args += argument.count
189 if len(arguments) < min_args \
190 or (max_args >= 0 and len(arguments) > max_args):
191 if min_args == max_args:
192 target_string = str(min_args)
194 target_string = 'more than %d' % min_args
196 target_string = '%d to %d' % (min_args, max_args)
197 raise optparse.OptParseError(
198 '%d arguments given, but %s takes %s'
199 % (len(arguments), self.name_fn(self.command.name),
202 def _handle_request(self, msg):
203 """Repeatedly try to get a response to `msg`.
205 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
207 raise NotImplementedError('_%s_request_prompt' % msg.type)
208 prompt_string = prompt(msg)
209 parser = getattr(self, '_%s_request_parser' % msg.type, None)
211 raise NotImplementedError('_%s_request_parser' % msg.type)
215 self.cmd.stdout.write(''.join([
216 error.__class__.__name__, ': ', str(error), '\n']))
217 self.cmd.stdout.write(prompt_string)
218 value = parser(msg, self.cmd.stdin.readline())
220 response = msg.response(value)
222 except ValueError, error:
224 self.cmd.inqueue.put(response)
226 def _boolean_request_prompt(self, msg):
227 if msg.default == True:
233 def _boolean_request_parser(self, msg, response):
234 value = response.strip().lower()
235 if value.startswith('y'):
237 elif value.startswith('n'):
239 elif len(value) == 0:
243 def _string_request_prompt(self, msg):
244 if msg.default == None:
247 d = ' [%s] ' % msg.default
250 def _string_request_parser(self, msg, response):
251 response = response.strip()
254 return response.strip()
256 def _float_request_prompt(self, msg):
257 return self._string_request_prompt(msg)
259 def _float_request_parser(self, msg, resposne):
260 if response.strip() == '':
262 return float(response)
264 def _selection_request_prompt(self, msg):
266 for i,option in enumerate(msg.options):
267 options.append(' %d) %s' % (i,option))
268 options = ''.join(options)
269 if msg.default == None:
272 prompt = '? [%d] ' % msg.default
273 return '\n'.join([msg.msg,options,prompt])
275 def _selection_request_parser(self, msg, response):
276 if response.strip() == '':
280 def _point_request_prompt(self, msg):
281 block = msg.curve.data[msg.block]
282 block_info = ('(curve: %s, block: %s, %d points)'
287 if msg.default == None:
290 prompt = '? [%d] ' % msg.default
291 return ' '.join([msg.msg,block_info,prompt])
293 def _point_request_parser(self, msg, response):
294 if response.strip() == '':
299 class HelpCommand (CommandMethod):
300 def __init__(self, *args, **kwargs):
301 super(HelpCommand, self).__init__(*args, **kwargs)
302 self.parser = CommandLineParser(self.command, self.name_fn)
305 blocks = [self.command.help(name_fn=self.name_fn),
307 'Usage: ' + self._usage_string(),
309 self.cmd.stdout.write('\n'.join(blocks))
312 return self.command.help(name_fn=self.name_fn)
314 def _usage_string(self):
315 if len(self.parser.command_opts) == 0:
318 options_string = '[options]'
319 arg_string = ' '.join(
320 [self.name_fn(arg.name) for arg in self.parser.command_args])
321 return ' '.join([x for x in [self.parser.prog,
326 class CompleteCommand (CommandMethod):
327 def __call__(self, text, line, begidx, endidx):
331 # Define some additional commands
333 class LocalHelpCommand (Command):
334 """Called with an argument, prints that command's documentation.
336 With no argument, lists all available help topics as well as any
337 undocumented commands.
340 super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
341 # We set .arguments now (vs. using th arguments option to __init__),
342 # to overwrite the default help argument. We don't override
343 # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
345 Argument(name='command', type='string', optional=True,
346 help='The name of the command you want help with.')
349 def _run(self, hooke, inqueue, outqueue, params):
350 raise NotImplementedError # cmd.Cmd already implements .do_help()
352 class LocalExitCommand (Command):
353 """Exit Hooke cleanly.
356 super(LocalExitCommand, self).__init__(
357 name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
359 Argument(name='force', type='bool', default=False,
361 Exit without prompting the user. Use if you save often or don't make
366 def _run(self, hooke, inqueue, outqueue, params):
367 """The guts of the `do_exit/_quit/_EOF` commands.
369 A `True` return stops :meth:`.cmdloop` execution.
372 if params['force'] == False:
373 not_saved = [p.name for p in hooke.playlists
374 if p.is_saved() == False]
377 if len(not_saved) > 0:
378 msg = 'Unsaved playlists (%s). %s' \
379 % (', '.join([str(p) for p in not_saved]), msg)
381 outqueue.put(BooleanRequest(msg, default))
382 result = inqueue.get()
383 assert result.type == 'boolean'
389 # Now onto the main attraction.
391 class HookeCmd (cmd.Cmd):
392 def __init__(self, ui, commands, inqueue, outqueue):
393 cmd.Cmd.__init__(self)
395 self.commands = commands
396 self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
397 self.prompt = 'hooke> '
398 self._add_command_methods()
399 self.inqueue = inqueue
400 self.outqueue = outqueue
402 def _name_fn(self, name):
403 return name.replace(' ', '_')
405 def _add_command_methods(self):
406 for command in self.commands + self.local_commands:
407 for name in [command.name] + command.aliases:
408 name = self._name_fn(name)
409 setattr(self.__class__, 'help_%s' % name,
410 HelpCommand(self, command, self._name_fn))
412 setattr(self.__class__, 'do_%s' % name,
413 DoCommand(self, command, self._name_fn))
414 setattr(self.__class__, 'complete_%s' % name,
415 CompleteCommand(self, command, self._name_fn))
417 def parseline(self, line):
418 """Override Cmd.parseline to use shlex.split.
422 This allows us to handle comments cleanly. With the default
423 Cmd implementation, a pure comment line will call the .default
426 Since we use shlex to strip comments, we return a list of
427 split arguments rather than the raw argument string.
430 argv = shlex.split(line, comments=True, posix=True)
432 return None, None, '' # return an empty line
439 return cmd, args, line
441 def do_help(self, arg):
442 """Wrap Cmd.do_help to handle our .parseline argument list.
445 return cmd.Cmd.do_help(self, '')
446 return cmd.Cmd.do_help(self, arg[0])
449 """Override Cmd.emptyline to not do anything.
451 Repeating the last non-empty command seems unwise. Explicit
452 is better than implicit.
457 class CommandLine (UserInterface):
458 """Command line interface. Simple and powerful.
461 super(CommandLine, self).__init__(name='command line')
463 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
464 cmd = HookeCmd(self, commands,
465 inqueue=ui_to_command_queue,
466 outqueue=command_to_ui_queue)
467 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
468 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
471 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
472 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
473 cmd.cmdloop(self._splash_text(extra_info={
474 'get-details':'run `license`',
477 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
479 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)