1 # Copyright (C) 2010-2012 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
29 import readline # including readline makes cmd.Cmd.cmdloop() smarter
30 except ImportError, e:
32 logging.warn('could not import readline, bash-like line editing disabled.')
36 from ..command import CommandExit, Exit, Command, Argument, StoreValue
37 from ..engine import CommandMessage, CloseEngine
38 from ..interaction import EOFResponse, Request, ReloadUserInterfaceConfig
39 from ..ui import UserInterface
40 from ..util.convert import from_string
41 from ..util.encoding import get_input_encoding, get_output_encoding
44 # Define a few helper classes.
47 """Raise upon reaching the end of the input file.
49 After this point, no more user interaction is possible.
53 class Default (object):
54 """Marker for options not given on the command line.
58 class CommandLineParser (optparse.OptionParser):
59 """Implement a command line syntax for a
60 :class:`hooke.command.Command`.
62 def __init__(self, command, name_fn):
63 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
64 self.command = command
65 self.command_opts = []
66 self.command_args = []
67 for a in command.arguments:
69 continue # 'help' is a default OptionParser option
70 if a.optional == True:
71 name = name_fn(a.name)
77 '--disable-%s' % name, dest=name,
78 default=Default, action='store_false',
79 help=self._argument_help(a))
80 except optparse.OptionConflictError, e:
81 logging.warn('error in %s: %s' % (command, e))
83 self.command_opts.append(a)
85 elif a.default == False:
88 '--enable-%s' % name, dest=name,
89 default=Default, action='store_true',
90 help=self._argument_help(a))
91 except optparse.OptionConflictError, e:
92 logging.warn('error in %s: %s' % (command, e))
94 self.command_opts.append(a)
98 elif type not in ['string', 'int', 'long', 'choice', 'float',
103 '--%s' % name, dest=name, type=type, default=Default,
104 help=self._argument_help(a))
105 except optparse.OptionConflictError, e:
106 logging.warn('error in %s: %s' % (command, e))
108 self.command_opts.append(a)
110 self.command_args.append(a)
111 infinite_counters = [a for a in self.command_args if a.count == -1]
112 assert len(infinite_counters) <= 1, \
113 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
114 % (command.name, ', '.join([a.name for a in infinite_counters]))
115 if len(infinite_counters) == 1: # move the big counter to the end.
116 infinite_counter = infinite_counters[0]
117 self.command_args.remove(infinite_counter)
118 self.command_args.append(infinite_counter)
120 def _argument_help(self, argument):
121 return '%s (%s)' % (argument._help, argument.default)
122 # default in the case of callbacks, config-backed values, etc.?
124 def exit(self, status=0, msg=None):
125 """Override :meth:`optparse.OptionParser.exit` which calls
129 raise optparse.OptParseError(msg)
130 raise optparse.OptParseError('OptParse EXIT')
132 class CommandMethod (object):
133 """Base class for method replacer.
135 The .__call__ methods of `CommandMethod` subclasses functions will
136 provide the `do_*`, `help_*`, and `complete_*` methods of
139 def __init__(self, cmd, command, name_fn):
141 self.command = command
142 self.name_fn = name_fn
144 def __call__(self, *args, **kwargs):
145 raise NotImplementedError
147 class DoCommand (CommandMethod):
148 def __init__(self, *args, **kwargs):
149 super(DoCommand, self).__init__(*args, **kwargs)
150 self.parser = CommandLineParser(self.command, self.name_fn)
152 def __call__(self, args):
154 args = self._parse_args(args)
155 except optparse.OptParseError, e:
156 self.cmd.stdout.write(unicode(e).lstrip()+'\n')
157 self.cmd.stdout.write('Failure\n')
159 cm = CommandMessage(self.command.name, args)
160 self.cmd.ui._submit_command(cm, self.cmd.inqueue)
162 msg = self.cmd.outqueue.get()
163 if isinstance(msg, Exit):
165 elif isinstance(msg, CommandExit):
166 self.cmd.stdout.write(msg.__class__.__name__+'\n')
167 self.cmd.stdout.write(unicode(msg).rstrip()+'\n')
169 elif isinstance(msg, ReloadUserInterfaceConfig):
170 self.cmd.ui.reload_config(msg.config)
172 elif isinstance(msg, Request):
174 self._handle_request(msg)
178 if isinstance(msg, dict):
179 text = pprint.pformat(msg)
182 self.cmd.stdout.write(text.rstrip()+'\n')
184 def _parse_args(self, args):
185 options,args = self.parser.parse_args(args)
186 self._check_argument_length_bounds(args)
188 for argument in self.parser.command_opts:
189 value = getattr(options, self.name_fn(argument.name))
191 params[argument.name] = value
193 for argument in self.parser.command_args:
194 if argument.count == 1:
195 params[argument.name] = from_string(args[arg_index],
197 elif argument.count > 1:
198 params[argument.name] = [
199 from_string(a, argument.type)
200 for a in args[arg_index:arg_index+argument.count]]
201 else: # argument.count == -1:
202 params[argument.name] = [
203 from_string(a, argument.type) for a in args[arg_index:]]
204 arg_index += argument.count
207 def _check_argument_length_bounds(self, arguments):
208 """Check that there are an appropriate number of arguments in
211 If not, raise optparse.OptParseError().
215 for argument in self.parser.command_args:
216 if argument.optional == False and argument.count > 0:
217 min_args += argument.count
218 if max_args >= 0: # otherwise already infinite
219 if argument.count == -1:
222 max_args += argument.count
223 if len(arguments) < min_args \
224 or (max_args >= 0 and len(arguments) > max_args):
225 if min_args == max_args:
226 target_string = str(min_args)
228 target_string = 'more than %d' % min_args
230 target_string = '%d to %d' % (min_args, max_args)
231 raise optparse.OptParseError(
232 '%d arguments given, but %s takes %s'
233 % (len(arguments), self.name_fn(self.command.name),
236 def _handle_request(self, msg):
237 """Repeatedly try to get a response to `msg`.
239 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
241 raise NotImplementedError('_%s_request_prompt' % msg.type)
242 prompt_string = prompt(msg)
243 parser = getattr(self, '_%s_request_parser' % msg.type, None)
245 raise NotImplementedError('_%s_request_parser' % msg.type)
249 self.cmd.stdout.write(''.join([
250 error.__class__.__name__, ': ', unicode(error), '\n']))
251 self.cmd.stdout.write(prompt_string)
254 sys.stdin = self.cmd.stdin
255 raw_response = raw_input()
257 self.cmd.inqueue.put(EOFResponse())
258 self.cmd.inqueue.put(CloseEngine())
262 value = parser(msg, raw_response)
264 response = msg.response(value)
266 except ValueError, error:
268 self.cmd.inqueue.put(response)
270 def _boolean_request_prompt(self, msg):
271 if msg.default == True:
277 def _boolean_request_parser(self, msg, response):
278 value = response.strip().lower()
279 if value.startswith('y'):
281 elif value.startswith('n'):
283 elif len(value) == 0:
287 def _string_request_prompt(self, msg):
288 if msg.default == None:
291 d = ' [%s] ' % msg.default
294 def _string_request_parser(self, msg, response):
295 response = response.strip()
298 return response.strip()
300 def _float_request_prompt(self, msg):
301 return self._string_request_prompt(msg)
303 def _float_request_parser(self, msg, resposne):
304 if response.strip() == '':
306 return float(response)
308 def _selection_request_prompt(self, msg):
310 for i,option in enumerate(msg.options):
311 options.append(' %d) %s' % (i,option))
312 options = ''.join(options)
313 if msg.default == None:
316 prompt = '? [%d] ' % msg.default
317 return '\n'.join([msg.msg,options,prompt])
319 def _selection_request_parser(self, msg, response):
320 if response.strip() == '':
324 def _point_request_prompt(self, msg):
325 block = msg.curve.data[msg.block]
326 block_info = ('(curve: %s, block: %s, %d points)'
331 if msg.default == None:
334 prompt = '? [%d] ' % msg.default
335 return ' '.join([msg.msg,block_info,prompt])
337 def _point_request_parser(self, msg, response):
338 if response.strip() == '':
343 class HelpCommand (CommandMethod):
344 """Supersedes :class:`hooke.plugin.engine.HelpCommand`.
346 def __init__(self, *args, **kwargs):
347 super(HelpCommand, self).__init__(*args, **kwargs)
348 self.parser = CommandLineParser(self.command, self.name_fn)
351 blocks = [self.parser.format_help(),
352 self._command_message(),
354 'Usage: ' + self._usage_string(),
356 self.cmd.stdout.write('\n'.join(blocks))
358 def _command_message(self):
359 return self.command._help
361 def _usage_string(self):
362 if len(self.parser.command_opts) == 0:
365 options_string = '[options]'
366 arg_string = ' '.join(
367 [self.name_fn(arg.name) for arg in self.parser.command_args])
368 return ' '.join([x for x in [self.parser.prog,
373 class CompleteCommand (CommandMethod):
374 def __call__(self, text, line, begidx, endidx):
379 # Now onto the main attraction.
381 class HookeCmd (cmd.Cmd):
382 def __init__(self, ui, commands, inqueue, outqueue):
383 cmd.Cmd.__init__(self)
385 self.commands = commands
386 self.prompt = 'hooke> '
387 self._add_command_methods()
388 self.inqueue = inqueue
389 self.outqueue = outqueue
391 def _name_fn(self, name):
392 return name.replace(' ', '_')
394 def _add_command_methods(self):
395 for command in self.commands:
396 if command.name == 'exit':
397 command.aliases.extend(['quit', 'EOF'])
398 for name in [command.name] + command.aliases:
399 name = self._name_fn(name)
400 setattr(self.__class__, 'help_%s' % name,
401 HelpCommand(self, command, self._name_fn))
403 setattr(self.__class__, 'do_%s' % name,
404 DoCommand(self, command, self._name_fn))
405 setattr(self.__class__, 'complete_%s' % name,
406 CompleteCommand(self, command, self._name_fn))
408 def parseline(self, line):
409 """Override Cmd.parseline to use shlex.split.
413 This allows us to handle comments cleanly. With the default
414 Cmd implementation, a pure comment line will call the .default
417 Since we use shlex to strip comments, we return a list of
418 split arguments rather than the raw argument string.
421 argv = shlex.split(line, comments=True, posix=True)
423 return None, None, '' # return an empty line
430 return cmd, args, line
432 def do_help(self, arg):
433 """Wrap Cmd.do_help to handle our .parseline argument list.
436 return cmd.Cmd.do_help(self, '')
437 return cmd.Cmd.do_help(self, arg[0])
440 """Override Cmd.emptyline to not do anything.
442 Repeating the last non-empty command seems unwise. Explicit
443 is better than implicit.
448 class CommandLine (UserInterface):
449 """Command line interface. Simple and powerful.
452 super(CommandLine, self).__init__(name='command line')
454 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
455 cmd = HookeCmd(self, commands,
456 inqueue=ui_to_command_queue,
457 outqueue=command_to_ui_queue)
458 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
459 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
462 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
463 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
464 cmd.cmdloop(self._splash_text(extra_info={
465 'get-details':'run `license`',
468 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
470 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)