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, 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)
68 '--disable-%s' % name, dest=name,
69 default=Default, action='store_false')
70 except optparse.OptionConflictError, e:
71 logging.warn('error in %s: %s' % (command, e))
73 self.command_opts.append(a)
75 elif a.default == False:
78 '--enable-%s' % name, dest=name,
79 default=Default, action='store_true')
80 except optparse.OptionConflictError, e:
81 logging.warn('error in %s: %s' % (command, e))
83 self.command_opts.append(a)
87 elif type not in ['string', 'int', 'long', 'choice', 'float',
92 '--%s' % name, dest=name, type=type, default=Default)
93 except optparse.OptionConflictError, e:
94 logging.warn('error in %s: %s' % (command, e))
96 self.command_opts.append(a)
98 self.command_args.append(a)
99 infinite_counters = [a for a in self.command_args if a.count == -1]
100 assert len(infinite_counters) <= 1, \
101 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
102 % (command.name, ', '.join([a.name for a in infinite_counters]))
103 if len(infinite_counters) == 1: # move the big counter to the end.
104 infinite_counter = infinite_counters[0]
105 self.command_args.remove(infinite_counter)
106 self.command_args.append(infinite_counter)
108 def exit(self, status=0, msg=None):
109 """Override :meth:`optparse.OptionParser.exit` which calls
113 raise optparse.OptParseError(msg)
114 raise optparse.OptParseError('OptParse EXIT')
116 class CommandMethod (object):
117 """Base class for method replacer.
119 The .__call__ methods of `CommandMethod` subclasses functions will
120 provide the `do_*`, `help_*`, and `complete_*` methods of
123 def __init__(self, cmd, command, name_fn):
125 self.command = command
126 self.name_fn = name_fn
128 def __call__(self, *args, **kwargs):
129 raise NotImplementedError
131 class DoCommand (CommandMethod):
132 def __init__(self, *args, **kwargs):
133 super(DoCommand, self).__init__(*args, **kwargs)
134 self.parser = CommandLineParser(self.command, self.name_fn)
136 def __call__(self, args):
138 args = self._parse_args(args)
139 except optparse.OptParseError, e:
140 self.cmd.stdout.write(str(e).lstrip()+'\n')
141 self.cmd.stdout.write('Failure\n')
143 cm = CommandMessage(self.command.name, args)
144 self.cmd.ui._submit_command(cm, self.cmd.inqueue)
146 msg = self.cmd.outqueue.get()
147 if isinstance(msg, Exit):
149 elif isinstance(msg, CommandExit):
150 self.cmd.stdout.write(msg.__class__.__name__+'\n')
151 self.cmd.stdout.write(str(msg).rstrip()+'\n')
153 elif isinstance(msg, ReloadUserInterfaceConfig):
154 self.cmd.ui.reload_config(msg.config)
156 elif isinstance(msg, Request):
157 self._handle_request(msg)
159 self.cmd.stdout.write(str(msg).rstrip()+'\n')
161 def _parse_args(self, args):
162 options,args = self.parser.parse_args(args)
163 self._check_argument_length_bounds(args)
165 for argument in self.parser.command_opts:
166 value = getattr(options, self.name_fn(argument.name))
168 params[argument.name] = value
170 for argument in self.parser.command_args:
171 if argument.count == 1:
172 params[argument.name] = from_string(args[arg_index],
174 elif argument.count > 1:
175 params[argument.name] = [
176 from_string(a, argument.type)
177 for a in args[arg_index:arg_index+argument.count]]
178 else: # argument.count == -1:
179 params[argument.name] = [
180 from_string(a, argument.type) for a in args[arg_index:]]
181 arg_index += argument.count
184 def _check_argument_length_bounds(self, arguments):
185 """Check that there are an appropriate number of arguments in
188 If not, raise optparse.OptParseError().
192 for argument in self.parser.command_args:
193 if argument.optional == False and argument.count > 0:
194 min_args += argument.count
195 if max_args >= 0: # otherwise already infinite
196 if argument.count == -1:
199 max_args += argument.count
200 if len(arguments) < min_args \
201 or (max_args >= 0 and len(arguments) > max_args):
202 if min_args == max_args:
203 target_string = str(min_args)
205 target_string = 'more than %d' % min_args
207 target_string = '%d to %d' % (min_args, max_args)
208 raise optparse.OptParseError(
209 '%d arguments given, but %s takes %s'
210 % (len(arguments), self.name_fn(self.command.name),
213 def _handle_request(self, msg):
214 """Repeatedly try to get a response to `msg`.
216 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
218 raise NotImplementedError('_%s_request_prompt' % msg.type)
219 prompt_string = prompt(msg)
220 parser = getattr(self, '_%s_request_parser' % msg.type, None)
222 raise NotImplementedError('_%s_request_parser' % msg.type)
226 self.cmd.stdout.write(''.join([
227 error.__class__.__name__, ': ', str(error), '\n']))
228 self.cmd.stdout.write(prompt_string)
229 value = parser(msg, self.cmd.stdin.readline())
231 response = msg.response(value)
233 except ValueError, error:
235 self.cmd.inqueue.put(response)
237 def _boolean_request_prompt(self, msg):
238 if msg.default == True:
244 def _boolean_request_parser(self, msg, response):
245 value = response.strip().lower()
246 if value.startswith('y'):
248 elif value.startswith('n'):
250 elif len(value) == 0:
254 def _string_request_prompt(self, msg):
255 if msg.default == None:
258 d = ' [%s] ' % msg.default
261 def _string_request_parser(self, msg, response):
262 response = response.strip()
265 return response.strip()
267 def _float_request_prompt(self, msg):
268 return self._string_request_prompt(msg)
270 def _float_request_parser(self, msg, resposne):
271 if response.strip() == '':
273 return float(response)
275 def _selection_request_prompt(self, msg):
277 for i,option in enumerate(msg.options):
278 options.append(' %d) %s' % (i,option))
279 options = ''.join(options)
280 if msg.default == None:
283 prompt = '? [%d] ' % msg.default
284 return '\n'.join([msg.msg,options,prompt])
286 def _selection_request_parser(self, msg, response):
287 if response.strip() == '':
291 def _point_request_prompt(self, msg):
292 block = msg.curve.data[msg.block]
293 block_info = ('(curve: %s, block: %s, %d points)'
298 if msg.default == None:
301 prompt = '? [%d] ' % msg.default
302 return ' '.join([msg.msg,block_info,prompt])
304 def _point_request_parser(self, msg, response):
305 if response.strip() == '':
310 class HelpCommand (CommandMethod):
311 """Supersedes :class:`hooke.plugin.engine.HelpCommand`.
313 def __init__(self, *args, **kwargs):
314 super(HelpCommand, self).__init__(*args, **kwargs)
315 self.parser = CommandLineParser(self.command, self.name_fn)
318 blocks = [self.command.help(name_fn=self.name_fn),
320 'Usage: ' + self._usage_string(),
322 self.cmd.stdout.write('\n'.join(blocks))
325 return self.command.help(name_fn=self.name_fn)
327 def _usage_string(self):
328 if len(self.parser.command_opts) == 0:
331 options_string = '[options]'
332 arg_string = ' '.join(
333 [self.name_fn(arg.name) for arg in self.parser.command_args])
334 return ' '.join([x for x in [self.parser.prog,
339 class CompleteCommand (CommandMethod):
340 def __call__(self, text, line, begidx, endidx):
345 # Now onto the main attraction.
347 class HookeCmd (cmd.Cmd):
348 def __init__(self, ui, commands, inqueue, outqueue):
349 cmd.Cmd.__init__(self)
351 self.commands = commands
352 self.prompt = 'hooke> '
353 self._add_command_methods()
354 self.inqueue = inqueue
355 self.outqueue = outqueue
357 def _name_fn(self, name):
358 return name.replace(' ', '_')
360 def _add_command_methods(self):
361 for command in self.commands:
362 if command.name == 'exit':
363 command.aliases.extend(['quit', 'EOF'])
364 for name in [command.name] + command.aliases:
365 name = self._name_fn(name)
366 setattr(self.__class__, 'help_%s' % name,
367 HelpCommand(self, command, self._name_fn))
369 setattr(self.__class__, 'do_%s' % name,
370 DoCommand(self, command, self._name_fn))
371 setattr(self.__class__, 'complete_%s' % name,
372 CompleteCommand(self, command, self._name_fn))
374 def parseline(self, line):
375 """Override Cmd.parseline to use shlex.split.
379 This allows us to handle comments cleanly. With the default
380 Cmd implementation, a pure comment line will call the .default
383 Since we use shlex to strip comments, we return a list of
384 split arguments rather than the raw argument string.
387 argv = shlex.split(line, comments=True, posix=True)
389 return None, None, '' # return an empty line
396 return cmd, args, line
398 def do_help(self, arg):
399 """Wrap Cmd.do_help to handle our .parseline argument list.
402 return cmd.Cmd.do_help(self, '')
403 return cmd.Cmd.do_help(self, arg[0])
406 """Override Cmd.emptyline to not do anything.
408 Repeating the last non-empty command seems unwise. Explicit
409 is better than implicit.
414 class CommandLine (UserInterface):
415 """Command line interface. Simple and powerful.
418 super(CommandLine, self).__init__(name='command line')
420 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
421 cmd = HookeCmd(self, commands,
422 inqueue=ui_to_command_queue,
423 outqueue=command_to_ui_queue)
424 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
425 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
428 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
429 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
430 cmd.cmdloop(self._splash_text(extra_info={
431 'get-details':'run `license`',
434 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
436 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)