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
27 import readline # including readline makes cmd.Cmd.cmdloop() smarter
28 except ImportError, e:
30 logging.warn('Could not import readline, bash-like line editing disabled.')
33 from ..command import CommandExit, Exit, Command, Argument, StoreValue
34 from ..interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
35 from ..ui import UserInterface, CommandMessage
36 from ..util.convert import from_string
37 from ..util.encoding import get_input_encoding, get_output_encoding
40 # Define a few helper classes.
42 class Default (object):
43 """Marker for options not given on the command line.
47 class CommandLineParser (optparse.OptionParser):
48 """Implement a command line syntax for a
49 :class:`hooke.command.Command`.
51 def __init__(self, command, name_fn):
52 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
53 self.command = command
54 self.command_opts = []
55 self.command_args = []
56 for a in command.arguments:
58 continue # 'help' is a default OptionParser option
59 if a.optional == True:
60 name = name_fn(a.name)
65 '--disable-%s' % name, dest=name, default=Default,
67 self.command_opts.append(a)
69 elif a.default == False:
71 '--enable-%s' % name, dest=name, default=Default,
73 self.command_opts.append(a)
77 elif type not in ['string', 'int', 'long', 'choice', 'float',
81 '--%s' % name, dest=name, type=type, default=Default)
82 self.command_opts.append(a)
84 self.command_args.append(a)
85 infinite_counters = [a for a in self.command_args if a.count == -1]
86 assert len(infinite_counters) <= 1, \
87 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
88 % (command.name, ', '.join([a.name for a in infinite_counters]))
89 if len(infinite_counters) == 1: # move the big counter to the end.
90 infinite_counter = infinite_counters[0]
91 self.command_args.remove(infinite_counter)
92 self.command_args.append(infinite_counter)
94 def exit(self, status=0, msg=None):
95 """Override :meth:`optparse.OptionParser.exit` which calls
99 raise optparse.OptParseError(msg)
100 raise optparse.OptParseError('OptParse EXIT')
102 class CommandMethod (object):
103 """Base class for method replacer.
105 The .__call__ methods of `CommandMethod` subclasses functions will
106 provide the `do_*`, `help_*`, and `complete_*` methods of
109 def __init__(self, cmd, command, name_fn):
111 self.command = command
112 self.name_fn = name_fn
114 def __call__(self, *args, **kwargs):
115 raise NotImplementedError
117 class DoCommand (CommandMethod):
118 def __init__(self, *args, **kwargs):
119 super(DoCommand, self).__init__(*args, **kwargs)
120 self.parser = CommandLineParser(self.command, self.name_fn)
122 def __call__(self, args):
124 args = self._parse_args(args)
125 except optparse.OptParseError, e:
126 self.cmd.stdout.write(str(e).lstrip()+'\n')
127 self.cmd.stdout.write('Failure\n')
130 self.cmd.inqueue.put(CommandMessage(self.command, args))
132 msg = self.cmd.outqueue.get()
133 if isinstance(msg, Exit):
135 elif isinstance(msg, CommandExit):
136 self.cmd.stdout.write(msg.__class__.__name__+'\n')
137 self.cmd.stdout.write(str(msg).rstrip()+'\n')
139 elif isinstance(msg, ReloadUserInterfaceConfig):
140 self.cmd.ui.reload_config(msg.config)
142 elif isinstance(msg, Request):
143 self._handle_request(msg)
145 self.cmd.stdout.write(str(msg).rstrip()+'\n')
147 def _parse_args(self, args):
148 options,args = self.parser.parse_args(args)
149 self._check_argument_length_bounds(args)
151 for argument in self.parser.command_opts:
152 value = getattr(options, self.name_fn(argument.name))
154 params[argument.name] = value
156 for argument in self.parser.command_args:
157 if argument.count == 1:
158 params[argument.name] = from_string(args[arg_index],
160 elif argument.count > 1:
161 params[argument.name] = [
162 from_string(a, argument.type)
163 for a in args[arg_index:arg_index+argument.count]]
164 else: # argument.count == -1:
165 params[argument.name] = [
166 from_string(a, argument.type) for a in args[arg_index:]]
167 arg_index += argument.count
170 def _check_argument_length_bounds(self, arguments):
171 """Check that there are an appropriate number of arguments in
174 If not, raise optparse.OptParseError().
178 for argument in self.parser.command_args:
179 if argument.optional == False and argument.count > 0:
180 min_args += argument.count
181 if max_args >= 0: # otherwise already infinite
182 if argument.count == -1:
185 max_args += argument.count
186 if len(arguments) < min_args \
187 or (max_args >= 0 and len(arguments) > max_args):
188 if min_args == max_args:
189 target_string = str(min_args)
191 target_string = 'more than %d' % min_args
193 target_string = '%d to %d' % (min_args, max_args)
194 raise optparse.OptParseError(
195 '%d arguments given, but %s takes %s'
196 % (len(arguments), self.name_fn(self.command.name),
199 def _handle_request(self, msg):
200 """Repeatedly try to get a response to `msg`.
202 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
204 raise NotImplementedError('_%s_request_prompt' % msg.type)
205 prompt_string = prompt(msg)
206 parser = getattr(self, '_%s_request_parser' % msg.type, None)
208 raise NotImplementedError('_%s_request_parser' % msg.type)
212 self.cmd.stdout.write(''.join([
213 error.__class__.__name__, ': ', str(error), '\n']))
214 self.cmd.stdout.write(prompt_string)
215 value = parser(msg, self.cmd.stdin.readline())
217 response = msg.response(value)
219 except ValueError, error:
221 self.cmd.inqueue.put(response)
223 def _boolean_request_prompt(self, msg):
224 if msg.default == True:
230 def _boolean_request_parser(self, msg, response):
231 value = response.strip().lower()
232 if value.startswith('y'):
234 elif value.startswith('n'):
236 elif len(value) == 0:
240 def _string_request_prompt(self, msg):
241 if msg.default == None:
244 d = ' [%s] ' % msg.default
247 def _string_request_parser(self, msg, response):
248 response = response.strip()
251 return response.strip()
253 def _float_request_prompt(self, msg):
254 return self._string_request_prompt(msg)
256 def _float_request_parser(self, msg, resposne):
257 if response.strip() == '':
259 return float(response)
261 def _selection_request_prompt(self, msg):
263 for i,option in enumerate(msg.options):
264 options.append(' %d) %s' % (i,option))
265 options = ''.join(options)
266 if msg.default == None:
269 prompt = '? [%d] ' % msg.default
270 return '\n'.join([msg.msg,options,prompt])
272 def _selection_request_parser(self, msg, response):
273 if response.strip() == '':
277 def _point_request_prompt(self, msg):
278 block = msg.curve.data[msg.block]
279 block_info = ('(curve: %s, block: %s, %d points)'
284 if msg.default == None:
287 prompt = '? [%d] ' % msg.default
288 return ' '.join([msg.msg,block_info,prompt])
290 def _point_request_parser(self, msg, response):
291 if response.strip() == '':
296 class HelpCommand (CommandMethod):
297 def __init__(self, *args, **kwargs):
298 super(HelpCommand, self).__init__(*args, **kwargs)
299 self.parser = CommandLineParser(self.command, self.name_fn)
302 blocks = [self.command.help(name_fn=self.name_fn),
304 'Usage: ' + self._usage_string(),
306 self.cmd.stdout.write('\n'.join(blocks))
309 return self.command.help(name_fn=self.name_fn)
311 def _usage_string(self):
312 if len(self.parser.command_opts) == 0:
315 options_string = '[options]'
316 arg_string = ' '.join(
317 [self.name_fn(arg.name) for arg in self.parser.command_args])
318 return ' '.join([x for x in [self.parser.prog,
323 class CompleteCommand (CommandMethod):
324 def __call__(self, text, line, begidx, endidx):
328 # Define some additional commands
330 class LocalHelpCommand (Command):
331 """Called with an argument, prints that command's documentation.
333 With no argument, lists all available help topics as well as any
334 undocumented commands.
337 super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
338 # We set .arguments now (vs. using th arguments option to __init__),
339 # to overwrite the default help argument. We don't override
340 # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
342 Argument(name='command', type='string', optional=True,
343 help='The name of the command you want help with.')
346 def _run(self, hooke, inqueue, outqueue, params):
347 raise NotImplementedError # cmd.Cmd already implements .do_help()
349 class LocalExitCommand (Command):
350 """Exit Hooke cleanly.
353 super(LocalExitCommand, self).__init__(
354 name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
356 Argument(name='force', type='bool', default=False,
358 Exit without prompting the user. Use if you save often or don't make
363 def _run(self, hooke, inqueue, outqueue, params):
364 """The guts of the `do_exit/_quit/_EOF` commands.
366 A `True` return stops :meth:`.cmdloop` execution.
369 if params['force'] == False:
370 not_saved = [p.name for p in hooke.playlists
371 if p.is_saved() == False]
374 if len(not_saved) > 0:
375 msg = 'Unsaved playlists (%s). %s' \
376 % (', '.join([str(p) for p in not_saved]), msg)
378 outqueue.put(BooleanRequest(msg, default))
379 result = inqueue.get()
380 assert result.type == 'boolean'
386 # Now onto the main attraction.
388 class HookeCmd (cmd.Cmd):
389 def __init__(self, ui, commands, inqueue, outqueue):
390 cmd.Cmd.__init__(self)
392 self.commands = commands
393 self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
394 self.prompt = 'hooke> '
395 self._add_command_methods()
396 self.inqueue = inqueue
397 self.outqueue = outqueue
399 def _name_fn(self, name):
400 return name.replace(' ', '_')
402 def _add_command_methods(self):
403 for command in self.commands + self.local_commands:
404 for name in [command.name] + command.aliases:
405 name = self._name_fn(name)
406 setattr(self.__class__, 'help_%s' % name,
407 HelpCommand(self, command, self._name_fn))
409 setattr(self.__class__, 'do_%s' % name,
410 DoCommand(self, command, self._name_fn))
411 setattr(self.__class__, 'complete_%s' % name,
412 CompleteCommand(self, command, self._name_fn))
414 def parseline(self, line):
415 """Override Cmd.parseline to use shlex.split.
419 This allows us to handle comments cleanly. With the default
420 Cmd implementation, a pure comment line will call the .default
423 Since we use shlex to strip comments, we return a list of
424 split arguments rather than the raw argument string.
427 argv = shlex.split(line, comments=True, posix=True)
429 return None, None, '' # return an empty line
436 return cmd, args, line
438 def do_help(self, arg):
439 """Wrap Cmd.do_help to handle our .parseline argument list.
442 return cmd.Cmd.do_help(self, '')
443 return cmd.Cmd.do_help(self, arg[0])
446 """Override Cmd.emptyline to not do anything.
448 Repeating the last non-empty command seems unwise. Explicit
449 is better than implicit.
454 class CommandLine (UserInterface):
455 """Command line interface. Simple and powerful.
458 super(CommandLine, self).__init__(name='command line')
460 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
461 cmd = HookeCmd(self, commands,
462 inqueue=ui_to_command_queue,
463 outqueue=command_to_ui_queue)
464 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
465 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
468 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
469 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
470 cmd.cmdloop(self._splash_text(extra_info={
471 'get-details':'run `license`',
474 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
476 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)