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 under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with Hooke. If not, see <http://www.gnu.org/licenses/>.
18 """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.')
35 from ..command import CommandExit, Exit, Command, Argument, StoreValue
36 from ..engine import CommandMessage, CloseEngine
37 from ..interaction import EOFResponse, Request, ReloadUserInterfaceConfig
38 from ..ui import UserInterface
39 from ..util.convert import from_string
40 from ..util.encoding import get_input_encoding, get_output_encoding
43 # Define a few helper classes.
46 """Raise upon reaching the end of the input file.
48 After this point, no more user interaction is possible.
52 class Default (object):
53 """Marker for options not given on the command line.
57 class CommandLineParser (optparse.OptionParser):
58 """Implement a command line syntax for a
59 :class:`hooke.command.Command`.
61 def __init__(self, command, name_fn):
62 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
63 self.command = command
64 self.command_opts = []
65 self.command_args = []
66 for a in command.arguments:
68 continue # 'help' is a default OptionParser option
69 if a.optional == True:
70 name = name_fn(a.name)
76 '--disable-%s' % name, dest=name,
77 default=Default, action='store_false',
78 help=self._argument_help(a))
79 except optparse.OptionConflictError, e:
80 logging.warn('error in %s: %s' % (command, e))
82 self.command_opts.append(a)
84 elif a.default == False:
87 '--enable-%s' % name, dest=name,
88 default=Default, action='store_true',
89 help=self._argument_help(a))
90 except optparse.OptionConflictError, e:
91 logging.warn('error in %s: %s' % (command, e))
93 self.command_opts.append(a)
97 elif type not in ['string', 'int', 'long', 'choice', 'float',
102 '--%s' % name, dest=name, type=type, default=Default,
103 help=self._argument_help(a))
104 except optparse.OptionConflictError, e:
105 logging.warn('error in %s: %s' % (command, e))
107 self.command_opts.append(a)
109 self.command_args.append(a)
110 infinite_counters = [a for a in self.command_args if a.count == -1]
111 assert len(infinite_counters) <= 1, \
112 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
113 % (command.name, ', '.join([a.name for a in infinite_counters]))
114 if len(infinite_counters) == 1: # move the big counter to the end.
115 infinite_counter = infinite_counters[0]
116 self.command_args.remove(infinite_counter)
117 self.command_args.append(infinite_counter)
119 def _argument_help(self, argument):
120 return '%s (%s)' % (argument._help, argument.default)
121 # default in the case of callbacks, config-backed values, etc.?
123 def exit(self, status=0, msg=None):
124 """Override :meth:`optparse.OptionParser.exit` which calls
128 raise optparse.OptParseError(msg)
129 raise optparse.OptParseError('OptParse EXIT')
131 class CommandMethod (object):
132 """Base class for method replacer.
134 The .__call__ methods of `CommandMethod` subclasses functions will
135 provide the `do_*`, `help_*`, and `complete_*` methods of
138 def __init__(self, cmd, command, name_fn):
140 self.command = command
141 self.name_fn = name_fn
143 def __call__(self, *args, **kwargs):
144 raise NotImplementedError
146 class DoCommand (CommandMethod):
147 def __init__(self, *args, **kwargs):
148 super(DoCommand, self).__init__(*args, **kwargs)
149 self.parser = CommandLineParser(self.command, self.name_fn)
151 def __call__(self, args):
153 args = self._parse_args(args)
154 except optparse.OptParseError, e:
155 self.cmd.stdout.write(unicode(e).lstrip()+'\n')
156 self.cmd.stdout.write('Failure\n')
158 cm = CommandMessage(self.command.name, args)
159 self.cmd.ui._submit_command(cm, self.cmd.inqueue)
161 msg = self.cmd.outqueue.get()
162 if isinstance(msg, Exit):
164 elif isinstance(msg, CommandExit):
165 self.cmd.stdout.write(msg.__class__.__name__+'\n')
166 self.cmd.stdout.write(unicode(msg).rstrip()+'\n')
168 elif isinstance(msg, ReloadUserInterfaceConfig):
169 self.cmd.ui.reload_config(msg.config)
171 elif isinstance(msg, Request):
173 self._handle_request(msg)
177 if isinstance(msg, dict):
178 text = pprint.pformat(msg)
181 self.cmd.stdout.write(text.rstrip()+'\n')
183 def _parse_args(self, args):
184 options,args = self.parser.parse_args(args)
185 self._check_argument_length_bounds(args)
187 for argument in self.parser.command_opts:
188 value = getattr(options, self.name_fn(argument.name))
190 params[argument.name] = value
192 for argument in self.parser.command_args:
193 if argument.count == 1:
194 params[argument.name] = from_string(args[arg_index],
196 elif argument.count > 1:
197 params[argument.name] = [
198 from_string(a, argument.type)
199 for a in args[arg_index:arg_index+argument.count]]
200 else: # argument.count == -1:
201 params[argument.name] = [
202 from_string(a, argument.type) for a in args[arg_index:]]
203 arg_index += argument.count
206 def _check_argument_length_bounds(self, arguments):
207 """Check that there are an appropriate number of arguments in
210 If not, raise optparse.OptParseError().
214 for argument in self.parser.command_args:
215 if argument.optional == False and argument.count > 0:
216 min_args += argument.count
217 if max_args >= 0: # otherwise already infinite
218 if argument.count == -1:
221 max_args += argument.count
222 if len(arguments) < min_args \
223 or (max_args >= 0 and len(arguments) > max_args):
224 if min_args == max_args:
225 target_string = str(min_args)
227 target_string = 'more than %d' % min_args
229 target_string = '%d to %d' % (min_args, max_args)
230 raise optparse.OptParseError(
231 '%d arguments given, but %s takes %s'
232 % (len(arguments), self.name_fn(self.command.name),
235 def _handle_request(self, msg):
236 """Repeatedly try to get a response to `msg`.
238 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
240 raise NotImplementedError('_%s_request_prompt' % msg.type)
241 prompt_string = prompt(msg)
242 parser = getattr(self, '_%s_request_parser' % msg.type, None)
244 raise NotImplementedError('_%s_request_parser' % msg.type)
248 self.cmd.stdout.write(''.join([
249 error.__class__.__name__, ': ', unicode(error), '\n']))
250 self.cmd.stdout.write(prompt_string)
253 sys.stdin = self.cmd.stdin
254 raw_response = raw_input()
256 self.cmd.inqueue.put(EOFResponse())
257 self.cmd.inqueue.put(CloseEngine())
261 value = parser(msg, raw_response)
263 response = msg.response(value)
265 except ValueError, error:
267 self.cmd.inqueue.put(response)
269 def _boolean_request_prompt(self, msg):
270 if msg.default == True:
276 def _boolean_request_parser(self, msg, response):
277 value = response.strip().lower()
278 if value.startswith('y'):
280 elif value.startswith('n'):
282 elif len(value) == 0:
286 def _string_request_prompt(self, msg):
287 if msg.default == None:
290 d = ' [%s] ' % msg.default
293 def _string_request_parser(self, msg, response):
294 response = response.strip()
297 return response.strip()
299 def _float_request_prompt(self, msg):
300 return self._string_request_prompt(msg)
302 def _float_request_parser(self, msg, resposne):
303 if response.strip() == '':
305 return float(response)
307 def _selection_request_prompt(self, msg):
309 for i,option in enumerate(msg.options):
310 options.append(' %d) %s' % (i,option))
311 options = ''.join(options)
312 if msg.default == None:
315 prompt = '? [%d] ' % msg.default
316 return '\n'.join([msg.msg,options,prompt])
318 def _selection_request_parser(self, msg, response):
319 if response.strip() == '':
323 def _point_request_prompt(self, msg):
324 block = msg.curve.data[msg.block]
325 block_info = ('(curve: %s, block: %s, %d points)'
330 if msg.default == None:
333 prompt = '? [%d] ' % msg.default
334 return ' '.join([msg.msg,block_info,prompt])
336 def _point_request_parser(self, msg, response):
337 if response.strip() == '':
342 class HelpCommand (CommandMethod):
343 """Supersedes :class:`hooke.plugin.engine.HelpCommand`.
345 def __init__(self, *args, **kwargs):
346 super(HelpCommand, self).__init__(*args, **kwargs)
347 self.parser = CommandLineParser(self.command, self.name_fn)
350 blocks = [self.parser.format_help(),
351 self._command_message(),
353 'Usage: ' + self._usage_string(),
355 self.cmd.stdout.write('\n'.join(blocks))
357 def _command_message(self):
358 return self.command._help
360 def _usage_string(self):
361 if len(self.parser.command_opts) == 0:
364 options_string = '[options]'
365 arg_string = ' '.join(
366 [self.name_fn(arg.name) for arg in self.parser.command_args])
367 return ' '.join([x for x in [self.parser.prog,
372 class CompleteCommand (CommandMethod):
373 def __call__(self, text, line, begidx, endidx):
378 # Now onto the main attraction.
380 class HookeCmd (cmd.Cmd):
381 def __init__(self, ui, commands, inqueue, outqueue):
382 cmd.Cmd.__init__(self)
384 self.commands = commands
385 self.prompt = 'hooke> '
386 self._add_command_methods()
387 self.inqueue = inqueue
388 self.outqueue = outqueue
390 def _name_fn(self, name):
391 return name.replace(' ', '_')
393 def _add_command_methods(self):
394 for command in self.commands:
395 if command.name == 'exit':
396 command.aliases.extend(['quit', 'EOF'])
397 for name in [command.name] + command.aliases:
398 name = self._name_fn(name)
399 setattr(self.__class__, 'help_%s' % name,
400 HelpCommand(self, command, self._name_fn))
402 setattr(self.__class__, 'do_%s' % name,
403 DoCommand(self, command, self._name_fn))
404 setattr(self.__class__, 'complete_%s' % name,
405 CompleteCommand(self, command, self._name_fn))
407 def parseline(self, line):
408 """Override Cmd.parseline to use shlex.split.
412 This allows us to handle comments cleanly. With the default
413 Cmd implementation, a pure comment line will call the .default
416 Since we use shlex to strip comments, we return a list of
417 split arguments rather than the raw argument string.
420 argv = shlex.split(line, comments=True, posix=True)
422 return None, None, '' # return an empty line
429 return cmd, args, line
431 def do_help(self, arg):
432 """Wrap Cmd.do_help to handle our .parseline argument list.
435 return cmd.Cmd.do_help(self, '')
436 return cmd.Cmd.do_help(self, arg[0])
439 """Override Cmd.emptyline to not do anything.
441 Repeating the last non-empty command seems unwise. Explicit
442 is better than implicit.
447 class CommandLine (UserInterface):
448 """Command line interface. Simple and powerful.
451 super(CommandLine, self).__init__(name='command line')
453 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
454 cmd = HookeCmd(self, commands,
455 inqueue=ui_to_command_queue,
456 outqueue=command_to_ui_queue)
457 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
458 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
461 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
462 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
463 cmd.cmdloop(self._splash_text(extra_info={
464 'get-details':'run `license`',
467 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
469 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)