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 except optparse.OptionConflictError, e:
79 logging.warn('error in %s: %s' % (command, e))
81 self.command_opts.append(a)
83 elif a.default == False:
86 '--enable-%s' % name, dest=name,
87 default=Default, action='store_true')
88 except optparse.OptionConflictError, e:
89 logging.warn('error in %s: %s' % (command, e))
91 self.command_opts.append(a)
95 elif type not in ['string', 'int', 'long', 'choice', 'float',
100 '--%s' % name, dest=name, type=type, default=Default)
101 except optparse.OptionConflictError, e:
102 logging.warn('error in %s: %s' % (command, e))
104 self.command_opts.append(a)
106 self.command_args.append(a)
107 infinite_counters = [a for a in self.command_args if a.count == -1]
108 assert len(infinite_counters) <= 1, \
109 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
110 % (command.name, ', '.join([a.name for a in infinite_counters]))
111 if len(infinite_counters) == 1: # move the big counter to the end.
112 infinite_counter = infinite_counters[0]
113 self.command_args.remove(infinite_counter)
114 self.command_args.append(infinite_counter)
116 def exit(self, status=0, msg=None):
117 """Override :meth:`optparse.OptionParser.exit` which calls
121 raise optparse.OptParseError(msg)
122 raise optparse.OptParseError('OptParse EXIT')
124 class CommandMethod (object):
125 """Base class for method replacer.
127 The .__call__ methods of `CommandMethod` subclasses functions will
128 provide the `do_*`, `help_*`, and `complete_*` methods of
131 def __init__(self, cmd, command, name_fn):
133 self.command = command
134 self.name_fn = name_fn
136 def __call__(self, *args, **kwargs):
137 raise NotImplementedError
139 class DoCommand (CommandMethod):
140 def __init__(self, *args, **kwargs):
141 super(DoCommand, self).__init__(*args, **kwargs)
142 self.parser = CommandLineParser(self.command, self.name_fn)
144 def __call__(self, args):
146 args = self._parse_args(args)
147 except optparse.OptParseError, e:
148 self.cmd.stdout.write(str(e).lstrip()+'\n')
149 self.cmd.stdout.write('Failure\n')
151 cm = CommandMessage(self.command.name, args)
152 self.cmd.ui._submit_command(cm, self.cmd.inqueue)
154 msg = self.cmd.outqueue.get()
155 if isinstance(msg, Exit):
157 elif isinstance(msg, CommandExit):
158 self.cmd.stdout.write(msg.__class__.__name__+'\n')
159 self.cmd.stdout.write(str(msg).rstrip()+'\n')
161 elif isinstance(msg, ReloadUserInterfaceConfig):
162 self.cmd.ui.reload_config(msg.config)
164 elif isinstance(msg, Request):
166 self._handle_request(msg)
170 self.cmd.stdout.write(unicode(msg).rstrip()+'\n')
172 def _parse_args(self, args):
173 options,args = self.parser.parse_args(args)
174 self._check_argument_length_bounds(args)
176 for argument in self.parser.command_opts:
177 value = getattr(options, self.name_fn(argument.name))
179 params[argument.name] = value
181 for argument in self.parser.command_args:
182 if argument.count == 1:
183 params[argument.name] = from_string(args[arg_index],
185 elif argument.count > 1:
186 params[argument.name] = [
187 from_string(a, argument.type)
188 for a in args[arg_index:arg_index+argument.count]]
189 else: # argument.count == -1:
190 params[argument.name] = [
191 from_string(a, argument.type) for a in args[arg_index:]]
192 arg_index += argument.count
195 def _check_argument_length_bounds(self, arguments):
196 """Check that there are an appropriate number of arguments in
199 If not, raise optparse.OptParseError().
203 for argument in self.parser.command_args:
204 if argument.optional == False and argument.count > 0:
205 min_args += argument.count
206 if max_args >= 0: # otherwise already infinite
207 if argument.count == -1:
210 max_args += argument.count
211 if len(arguments) < min_args \
212 or (max_args >= 0 and len(arguments) > max_args):
213 if min_args == max_args:
214 target_string = str(min_args)
216 target_string = 'more than %d' % min_args
218 target_string = '%d to %d' % (min_args, max_args)
219 raise optparse.OptParseError(
220 '%d arguments given, but %s takes %s'
221 % (len(arguments), self.name_fn(self.command.name),
224 def _handle_request(self, msg):
225 """Repeatedly try to get a response to `msg`.
227 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
229 raise NotImplementedError('_%s_request_prompt' % msg.type)
230 prompt_string = prompt(msg)
231 parser = getattr(self, '_%s_request_parser' % msg.type, None)
233 raise NotImplementedError('_%s_request_parser' % msg.type)
237 self.cmd.stdout.write(''.join([
238 error.__class__.__name__, ': ', str(error), '\n']))
239 self.cmd.stdout.write(prompt_string)
242 sys.stdin = self.cmd.stdin
243 raw_response = raw_input()
245 self.cmd.inqueue.put(EOFResponse())
246 self.cmd.inqueue.put(CloseEngine())
250 value = parser(msg, raw_response)
252 response = msg.response(value)
254 except ValueError, error:
256 self.cmd.inqueue.put(response)
258 def _boolean_request_prompt(self, msg):
259 if msg.default == True:
265 def _boolean_request_parser(self, msg, response):
266 value = response.strip().lower()
267 if value.startswith('y'):
269 elif value.startswith('n'):
271 elif len(value) == 0:
275 def _string_request_prompt(self, msg):
276 if msg.default == None:
279 d = ' [%s] ' % msg.default
282 def _string_request_parser(self, msg, response):
283 response = response.strip()
286 return response.strip()
288 def _float_request_prompt(self, msg):
289 return self._string_request_prompt(msg)
291 def _float_request_parser(self, msg, resposne):
292 if response.strip() == '':
294 return float(response)
296 def _selection_request_prompt(self, msg):
298 for i,option in enumerate(msg.options):
299 options.append(' %d) %s' % (i,option))
300 options = ''.join(options)
301 if msg.default == None:
304 prompt = '? [%d] ' % msg.default
305 return '\n'.join([msg.msg,options,prompt])
307 def _selection_request_parser(self, msg, response):
308 if response.strip() == '':
312 def _point_request_prompt(self, msg):
313 block = msg.curve.data[msg.block]
314 block_info = ('(curve: %s, block: %s, %d points)'
319 if msg.default == None:
322 prompt = '? [%d] ' % msg.default
323 return ' '.join([msg.msg,block_info,prompt])
325 def _point_request_parser(self, msg, response):
326 if response.strip() == '':
331 class HelpCommand (CommandMethod):
332 """Supersedes :class:`hooke.plugin.engine.HelpCommand`.
334 def __init__(self, *args, **kwargs):
335 super(HelpCommand, self).__init__(*args, **kwargs)
336 self.parser = CommandLineParser(self.command, self.name_fn)
339 blocks = [self.command.help(name_fn=self.name_fn),
341 'Usage: ' + self._usage_string(),
343 self.cmd.stdout.write('\n'.join(blocks))
346 return self.command.help(name_fn=self.name_fn)
348 def _usage_string(self):
349 if len(self.parser.command_opts) == 0:
352 options_string = '[options]'
353 arg_string = ' '.join(
354 [self.name_fn(arg.name) for arg in self.parser.command_args])
355 return ' '.join([x for x in [self.parser.prog,
360 class CompleteCommand (CommandMethod):
361 def __call__(self, text, line, begidx, endidx):
366 # Now onto the main attraction.
368 class HookeCmd (cmd.Cmd):
369 def __init__(self, ui, commands, inqueue, outqueue):
370 cmd.Cmd.__init__(self)
372 self.commands = commands
373 self.prompt = 'hooke> '
374 self._add_command_methods()
375 self.inqueue = inqueue
376 self.outqueue = outqueue
378 def _name_fn(self, name):
379 return name.replace(' ', '_')
381 def _add_command_methods(self):
382 for command in self.commands:
383 if command.name == 'exit':
384 command.aliases.extend(['quit', 'EOF'])
385 for name in [command.name] + command.aliases:
386 name = self._name_fn(name)
387 setattr(self.__class__, 'help_%s' % name,
388 HelpCommand(self, command, self._name_fn))
390 setattr(self.__class__, 'do_%s' % name,
391 DoCommand(self, command, self._name_fn))
392 setattr(self.__class__, 'complete_%s' % name,
393 CompleteCommand(self, command, self._name_fn))
395 def parseline(self, line):
396 """Override Cmd.parseline to use shlex.split.
400 This allows us to handle comments cleanly. With the default
401 Cmd implementation, a pure comment line will call the .default
404 Since we use shlex to strip comments, we return a list of
405 split arguments rather than the raw argument string.
408 argv = shlex.split(line, comments=True, posix=True)
410 return None, None, '' # return an empty line
417 return cmd, args, line
419 def do_help(self, arg):
420 """Wrap Cmd.do_help to handle our .parseline argument list.
423 return cmd.Cmd.do_help(self, '')
424 return cmd.Cmd.do_help(self, arg[0])
427 """Override Cmd.emptyline to not do anything.
429 Repeating the last non-empty command seems unwise. Explicit
430 is better than implicit.
435 class CommandLine (UserInterface):
436 """Command line interface. Simple and powerful.
439 super(CommandLine, self).__init__(name='command line')
441 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
442 cmd = HookeCmd(self, commands,
443 inqueue=ui_to_command_queue,
444 outqueue=command_to_ui_queue)
445 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
446 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
449 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
450 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
451 cmd.cmdloop(self._splash_text(extra_info={
452 'get-details':'run `license`',
455 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
457 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)