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.')
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(str(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(str(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 self.cmd.stdout.write(unicode(msg).rstrip()+'\n')
179 def _parse_args(self, args):
180 options,args = self.parser.parse_args(args)
181 self._check_argument_length_bounds(args)
183 for argument in self.parser.command_opts:
184 value = getattr(options, self.name_fn(argument.name))
186 params[argument.name] = value
188 for argument in self.parser.command_args:
189 if argument.count == 1:
190 params[argument.name] = from_string(args[arg_index],
192 elif argument.count > 1:
193 params[argument.name] = [
194 from_string(a, argument.type)
195 for a in args[arg_index:arg_index+argument.count]]
196 else: # argument.count == -1:
197 params[argument.name] = [
198 from_string(a, argument.type) for a in args[arg_index:]]
199 arg_index += argument.count
202 def _check_argument_length_bounds(self, arguments):
203 """Check that there are an appropriate number of arguments in
206 If not, raise optparse.OptParseError().
210 for argument in self.parser.command_args:
211 if argument.optional == False and argument.count > 0:
212 min_args += argument.count
213 if max_args >= 0: # otherwise already infinite
214 if argument.count == -1:
217 max_args += argument.count
218 if len(arguments) < min_args \
219 or (max_args >= 0 and len(arguments) > max_args):
220 if min_args == max_args:
221 target_string = str(min_args)
223 target_string = 'more than %d' % min_args
225 target_string = '%d to %d' % (min_args, max_args)
226 raise optparse.OptParseError(
227 '%d arguments given, but %s takes %s'
228 % (len(arguments), self.name_fn(self.command.name),
231 def _handle_request(self, msg):
232 """Repeatedly try to get a response to `msg`.
234 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
236 raise NotImplementedError('_%s_request_prompt' % msg.type)
237 prompt_string = prompt(msg)
238 parser = getattr(self, '_%s_request_parser' % msg.type, None)
240 raise NotImplementedError('_%s_request_parser' % msg.type)
244 self.cmd.stdout.write(''.join([
245 error.__class__.__name__, ': ', str(error), '\n']))
246 self.cmd.stdout.write(prompt_string)
249 sys.stdin = self.cmd.stdin
250 raw_response = raw_input()
252 self.cmd.inqueue.put(EOFResponse())
253 self.cmd.inqueue.put(CloseEngine())
257 value = parser(msg, raw_response)
259 response = msg.response(value)
261 except ValueError, error:
263 self.cmd.inqueue.put(response)
265 def _boolean_request_prompt(self, msg):
266 if msg.default == True:
272 def _boolean_request_parser(self, msg, response):
273 value = response.strip().lower()
274 if value.startswith('y'):
276 elif value.startswith('n'):
278 elif len(value) == 0:
282 def _string_request_prompt(self, msg):
283 if msg.default == None:
286 d = ' [%s] ' % msg.default
289 def _string_request_parser(self, msg, response):
290 response = response.strip()
293 return response.strip()
295 def _float_request_prompt(self, msg):
296 return self._string_request_prompt(msg)
298 def _float_request_parser(self, msg, resposne):
299 if response.strip() == '':
301 return float(response)
303 def _selection_request_prompt(self, msg):
305 for i,option in enumerate(msg.options):
306 options.append(' %d) %s' % (i,option))
307 options = ''.join(options)
308 if msg.default == None:
311 prompt = '? [%d] ' % msg.default
312 return '\n'.join([msg.msg,options,prompt])
314 def _selection_request_parser(self, msg, response):
315 if response.strip() == '':
319 def _point_request_prompt(self, msg):
320 block = msg.curve.data[msg.block]
321 block_info = ('(curve: %s, block: %s, %d points)'
326 if msg.default == None:
329 prompt = '? [%d] ' % msg.default
330 return ' '.join([msg.msg,block_info,prompt])
332 def _point_request_parser(self, msg, response):
333 if response.strip() == '':
338 class HelpCommand (CommandMethod):
339 """Supersedes :class:`hooke.plugin.engine.HelpCommand`.
341 def __init__(self, *args, **kwargs):
342 super(HelpCommand, self).__init__(*args, **kwargs)
343 self.parser = CommandLineParser(self.command, self.name_fn)
346 blocks = [self.parser.format_help(),
347 self._command_message(),
349 'Usage: ' + self._usage_string(),
351 self.cmd.stdout.write('\n'.join(blocks))
353 def _command_message(self):
354 return self.command._help
356 def _usage_string(self):
357 if len(self.parser.command_opts) == 0:
360 options_string = '[options]'
361 arg_string = ' '.join(
362 [self.name_fn(arg.name) for arg in self.parser.command_args])
363 return ' '.join([x for x in [self.parser.prog,
368 class CompleteCommand (CommandMethod):
369 def __call__(self, text, line, begidx, endidx):
374 # Now onto the main attraction.
376 class HookeCmd (cmd.Cmd):
377 def __init__(self, ui, commands, inqueue, outqueue):
378 cmd.Cmd.__init__(self)
380 self.commands = commands
381 self.prompt = 'hooke> '
382 self._add_command_methods()
383 self.inqueue = inqueue
384 self.outqueue = outqueue
386 def _name_fn(self, name):
387 return name.replace(' ', '_')
389 def _add_command_methods(self):
390 for command in self.commands:
391 if command.name == 'exit':
392 command.aliases.extend(['quit', 'EOF'])
393 for name in [command.name] + command.aliases:
394 name = self._name_fn(name)
395 setattr(self.__class__, 'help_%s' % name,
396 HelpCommand(self, command, self._name_fn))
398 setattr(self.__class__, 'do_%s' % name,
399 DoCommand(self, command, self._name_fn))
400 setattr(self.__class__, 'complete_%s' % name,
401 CompleteCommand(self, command, self._name_fn))
403 def parseline(self, line):
404 """Override Cmd.parseline to use shlex.split.
408 This allows us to handle comments cleanly. With the default
409 Cmd implementation, a pure comment line will call the .default
412 Since we use shlex to strip comments, we return a list of
413 split arguments rather than the raw argument string.
416 argv = shlex.split(line, comments=True, posix=True)
418 return None, None, '' # return an empty line
425 return cmd, args, line
427 def do_help(self, arg):
428 """Wrap Cmd.do_help to handle our .parseline argument list.
431 return cmd.Cmd.do_help(self, '')
432 return cmd.Cmd.do_help(self, arg[0])
435 """Override Cmd.emptyline to not do anything.
437 Repeating the last non-empty command seems unwise. Explicit
438 is better than implicit.
443 class CommandLine (UserInterface):
444 """Command line interface. Simple and powerful.
447 super(CommandLine, self).__init__(name='command line')
449 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
450 cmd = HookeCmd(self, commands,
451 inqueue=ui_to_command_queue,
452 outqueue=command_to_ui_queue)
453 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
454 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
457 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
458 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
459 cmd.cmdloop(self._splash_text(extra_info={
460 'get-details':'run `license`',
463 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
465 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)