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)
67 '--disable-%s' % name, dest=name, default=Default,
69 self.command_opts.append(a)
71 elif a.default == False:
73 '--enable-%s' % name, dest=name, default=Default,
75 self.command_opts.append(a)
79 elif type not in ['string', 'int', 'long', 'choice', 'float',
83 '--%s' % name, dest=name, type=type, default=Default)
84 self.command_opts.append(a)
86 self.command_args.append(a)
87 infinite_counters = [a for a in self.command_args if a.count == -1]
88 assert len(infinite_counters) <= 1, \
89 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
90 % (command.name, ', '.join([a.name for a in infinite_counters]))
91 if len(infinite_counters) == 1: # move the big counter to the end.
92 infinite_counter = infinite_counters[0]
93 self.command_args.remove(infinite_counter)
94 self.command_args.append(infinite_counter)
96 def exit(self, status=0, msg=None):
97 """Override :meth:`optparse.OptionParser.exit` which calls
101 raise optparse.OptParseError(msg)
102 raise optparse.OptParseError('OptParse EXIT')
104 class CommandMethod (object):
105 """Base class for method replacer.
107 The .__call__ methods of `CommandMethod` subclasses functions will
108 provide the `do_*`, `help_*`, and `complete_*` methods of
111 def __init__(self, cmd, command, name_fn):
113 self.command = command
114 self.name_fn = name_fn
116 def __call__(self, *args, **kwargs):
117 raise NotImplementedError
119 class DoCommand (CommandMethod):
120 def __init__(self, *args, **kwargs):
121 super(DoCommand, self).__init__(*args, **kwargs)
122 self.parser = CommandLineParser(self.command, self.name_fn)
123 self.log = logging.getLogger('hooke')
125 def __call__(self, args):
127 args = self._parse_args(args)
128 except optparse.OptParseError, e:
129 self.cmd.stdout.write(str(e).lstrip()+'\n')
130 self.cmd.stdout.write('Failure\n')
132 cm = CommandMessage(self.command.name, args)
133 self.log.debug('executing %s' % cm)
134 self.cmd.inqueue.put(cm)
136 msg = self.cmd.outqueue.get()
137 if isinstance(msg, Exit):
139 elif isinstance(msg, CommandExit):
140 self.cmd.stdout.write(msg.__class__.__name__+'\n')
141 self.cmd.stdout.write(str(msg).rstrip()+'\n')
143 elif isinstance(msg, ReloadUserInterfaceConfig):
144 self.cmd.ui.reload_config(msg.config)
146 elif isinstance(msg, Request):
147 self._handle_request(msg)
149 self.cmd.stdout.write(str(msg).rstrip()+'\n')
151 def _parse_args(self, args):
152 options,args = self.parser.parse_args(args)
153 self._check_argument_length_bounds(args)
155 for argument in self.parser.command_opts:
156 value = getattr(options, self.name_fn(argument.name))
158 params[argument.name] = value
160 for argument in self.parser.command_args:
161 if argument.count == 1:
162 params[argument.name] = from_string(args[arg_index],
164 elif argument.count > 1:
165 params[argument.name] = [
166 from_string(a, argument.type)
167 for a in args[arg_index:arg_index+argument.count]]
168 else: # argument.count == -1:
169 params[argument.name] = [
170 from_string(a, argument.type) for a in args[arg_index:]]
171 arg_index += argument.count
174 def _check_argument_length_bounds(self, arguments):
175 """Check that there are an appropriate number of arguments in
178 If not, raise optparse.OptParseError().
182 for argument in self.parser.command_args:
183 if argument.optional == False and argument.count > 0:
184 min_args += argument.count
185 if max_args >= 0: # otherwise already infinite
186 if argument.count == -1:
189 max_args += argument.count
190 if len(arguments) < min_args \
191 or (max_args >= 0 and len(arguments) > max_args):
192 if min_args == max_args:
193 target_string = str(min_args)
195 target_string = 'more than %d' % min_args
197 target_string = '%d to %d' % (min_args, max_args)
198 raise optparse.OptParseError(
199 '%d arguments given, but %s takes %s'
200 % (len(arguments), self.name_fn(self.command.name),
203 def _handle_request(self, msg):
204 """Repeatedly try to get a response to `msg`.
206 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
208 raise NotImplementedError('_%s_request_prompt' % msg.type)
209 prompt_string = prompt(msg)
210 parser = getattr(self, '_%s_request_parser' % msg.type, None)
212 raise NotImplementedError('_%s_request_parser' % msg.type)
216 self.cmd.stdout.write(''.join([
217 error.__class__.__name__, ': ', str(error), '\n']))
218 self.cmd.stdout.write(prompt_string)
219 value = parser(msg, self.cmd.stdin.readline())
221 response = msg.response(value)
223 except ValueError, error:
225 self.cmd.inqueue.put(response)
227 def _boolean_request_prompt(self, msg):
228 if msg.default == True:
234 def _boolean_request_parser(self, msg, response):
235 value = response.strip().lower()
236 if value.startswith('y'):
238 elif value.startswith('n'):
240 elif len(value) == 0:
244 def _string_request_prompt(self, msg):
245 if msg.default == None:
248 d = ' [%s] ' % msg.default
251 def _string_request_parser(self, msg, response):
252 response = response.strip()
255 return response.strip()
257 def _float_request_prompt(self, msg):
258 return self._string_request_prompt(msg)
260 def _float_request_parser(self, msg, resposne):
261 if response.strip() == '':
263 return float(response)
265 def _selection_request_prompt(self, msg):
267 for i,option in enumerate(msg.options):
268 options.append(' %d) %s' % (i,option))
269 options = ''.join(options)
270 if msg.default == None:
273 prompt = '? [%d] ' % msg.default
274 return '\n'.join([msg.msg,options,prompt])
276 def _selection_request_parser(self, msg, response):
277 if response.strip() == '':
281 def _point_request_prompt(self, msg):
282 block = msg.curve.data[msg.block]
283 block_info = ('(curve: %s, block: %s, %d points)'
288 if msg.default == None:
291 prompt = '? [%d] ' % msg.default
292 return ' '.join([msg.msg,block_info,prompt])
294 def _point_request_parser(self, msg, response):
295 if response.strip() == '':
300 class HelpCommand (CommandMethod):
301 """Supersedes :class:`hooke.plugin.engine.HelpCommand`.
303 def __init__(self, *args, **kwargs):
304 super(HelpCommand, self).__init__(*args, **kwargs)
305 self.parser = CommandLineParser(self.command, self.name_fn)
308 blocks = [self.command.help(name_fn=self.name_fn),
310 'Usage: ' + self._usage_string(),
312 self.cmd.stdout.write('\n'.join(blocks))
315 return self.command.help(name_fn=self.name_fn)
317 def _usage_string(self):
318 if len(self.parser.command_opts) == 0:
321 options_string = '[options]'
322 arg_string = ' '.join(
323 [self.name_fn(arg.name) for arg in self.parser.command_args])
324 return ' '.join([x for x in [self.parser.prog,
329 class CompleteCommand (CommandMethod):
330 def __call__(self, text, line, begidx, endidx):
335 # Now onto the main attraction.
337 class HookeCmd (cmd.Cmd):
338 def __init__(self, ui, commands, inqueue, outqueue):
339 cmd.Cmd.__init__(self)
341 self.commands = commands
342 self.prompt = 'hooke> '
343 self._add_command_methods()
344 self.inqueue = inqueue
345 self.outqueue = outqueue
347 def _name_fn(self, name):
348 return name.replace(' ', '_')
350 def _add_command_methods(self):
351 for command in self.commands:
352 if command.name == 'exit':
353 command.aliases.extend(['quit', 'EOF'])
354 for name in [command.name] + command.aliases:
355 name = self._name_fn(name)
356 setattr(self.__class__, 'help_%s' % name,
357 HelpCommand(self, command, self._name_fn))
359 setattr(self.__class__, 'do_%s' % name,
360 DoCommand(self, command, self._name_fn))
361 setattr(self.__class__, 'complete_%s' % name,
362 CompleteCommand(self, command, self._name_fn))
364 def parseline(self, line):
365 """Override Cmd.parseline to use shlex.split.
369 This allows us to handle comments cleanly. With the default
370 Cmd implementation, a pure comment line will call the .default
373 Since we use shlex to strip comments, we return a list of
374 split arguments rather than the raw argument string.
377 argv = shlex.split(line, comments=True, posix=True)
379 return None, None, '' # return an empty line
386 return cmd, args, line
388 def do_help(self, arg):
389 """Wrap Cmd.do_help to handle our .parseline argument list.
392 return cmd.Cmd.do_help(self, '')
393 return cmd.Cmd.do_help(self, arg[0])
396 """Override Cmd.emptyline to not do anything.
398 Repeating the last non-empty command seems unwise. Explicit
399 is better than implicit.
404 class CommandLine (UserInterface):
405 """Command line interface. Simple and powerful.
408 super(CommandLine, self).__init__(name='command line')
410 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
411 cmd = HookeCmd(self, commands,
412 inqueue=ui_to_command_queue,
413 outqueue=command_to_ui_queue)
414 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
415 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
418 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
419 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
420 cmd.cmdloop(self._splash_text(extra_info={
421 'get-details':'run `license`',
424 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
426 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)