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 ..interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
36 from ..ui import UserInterface, CommandMessage
37 from ..util.convert import from_string
38 from ..util.encoding import get_input_encoding, get_output_encoding
41 # Define a few helper classes.
43 class Default (object):
44 """Marker for options not given on the command line.
48 class CommandLineParser (optparse.OptionParser):
49 """Implement a command line syntax for a
50 :class:`hooke.command.Command`.
52 def __init__(self, command, name_fn):
53 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
54 self.command = command
55 self.command_opts = []
56 self.command_args = []
57 for a in command.arguments:
59 continue # 'help' is a default OptionParser option
60 if a.optional == True:
61 name = name_fn(a.name)
66 '--disable-%s' % name, dest=name, default=Default,
68 self.command_opts.append(a)
70 elif a.default == False:
72 '--enable-%s' % name, dest=name, default=Default,
74 self.command_opts.append(a)
78 elif type not in ['string', 'int', 'long', 'choice', 'float',
82 '--%s' % name, dest=name, type=type, default=Default)
83 self.command_opts.append(a)
85 self.command_args.append(a)
86 infinite_counters = [a for a in self.command_args if a.count == -1]
87 assert len(infinite_counters) <= 1, \
88 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
89 % (command.name, ', '.join([a.name for a in infinite_counters]))
90 if len(infinite_counters) == 1: # move the big counter to the end.
91 infinite_counter = infinite_counters[0]
92 self.command_args.remove(infinite_counter)
93 self.command_args.append(infinite_counter)
95 def exit(self, status=0, msg=None):
96 """Override :meth:`optparse.OptionParser.exit` which calls
100 raise optparse.OptParseError(msg)
101 raise optparse.OptParseError('OptParse EXIT')
103 class CommandMethod (object):
104 """Base class for method replacer.
106 The .__call__ methods of `CommandMethod` subclasses functions will
107 provide the `do_*`, `help_*`, and `complete_*` methods of
110 def __init__(self, cmd, command, name_fn):
112 self.command = command
113 self.name_fn = name_fn
115 def __call__(self, *args, **kwargs):
116 raise NotImplementedError
118 class DoCommand (CommandMethod):
119 def __init__(self, *args, **kwargs):
120 super(DoCommand, self).__init__(*args, **kwargs)
121 self.parser = CommandLineParser(self.command, self.name_fn)
122 self.log = logging.getLogger('hooke')
124 def __call__(self, args):
126 args = self._parse_args(args)
127 except optparse.OptParseError, e:
128 self.cmd.stdout.write(str(e).lstrip()+'\n')
129 self.cmd.stdout.write('Failure\n')
131 self.log.debug('executing %s with %s' % (self.command.name, args))
132 self.cmd.inqueue.put(CommandMessage(self.command, args))
134 msg = self.cmd.outqueue.get()
135 if isinstance(msg, Exit):
137 elif isinstance(msg, CommandExit):
138 self.cmd.stdout.write(msg.__class__.__name__+'\n')
139 self.cmd.stdout.write(str(msg).rstrip()+'\n')
141 elif isinstance(msg, ReloadUserInterfaceConfig):
142 self.cmd.ui.reload_config(msg.config)
144 elif isinstance(msg, Request):
145 self._handle_request(msg)
147 self.cmd.stdout.write(str(msg).rstrip()+'\n')
149 def _parse_args(self, args):
150 options,args = self.parser.parse_args(args)
151 self._check_argument_length_bounds(args)
153 for argument in self.parser.command_opts:
154 value = getattr(options, self.name_fn(argument.name))
156 params[argument.name] = value
158 for argument in self.parser.command_args:
159 if argument.count == 1:
160 params[argument.name] = from_string(args[arg_index],
162 elif argument.count > 1:
163 params[argument.name] = [
164 from_string(a, argument.type)
165 for a in args[arg_index:arg_index+argument.count]]
166 else: # argument.count == -1:
167 params[argument.name] = [
168 from_string(a, argument.type) for a in args[arg_index:]]
169 arg_index += argument.count
172 def _check_argument_length_bounds(self, arguments):
173 """Check that there are an appropriate number of arguments in
176 If not, raise optparse.OptParseError().
180 for argument in self.parser.command_args:
181 if argument.optional == False and argument.count > 0:
182 min_args += argument.count
183 if max_args >= 0: # otherwise already infinite
184 if argument.count == -1:
187 max_args += argument.count
188 if len(arguments) < min_args \
189 or (max_args >= 0 and len(arguments) > max_args):
190 if min_args == max_args:
191 target_string = str(min_args)
193 target_string = 'more than %d' % min_args
195 target_string = '%d to %d' % (min_args, max_args)
196 raise optparse.OptParseError(
197 '%d arguments given, but %s takes %s'
198 % (len(arguments), self.name_fn(self.command.name),
201 def _handle_request(self, msg):
202 """Repeatedly try to get a response to `msg`.
204 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
206 raise NotImplementedError('_%s_request_prompt' % msg.type)
207 prompt_string = prompt(msg)
208 parser = getattr(self, '_%s_request_parser' % msg.type, None)
210 raise NotImplementedError('_%s_request_parser' % msg.type)
214 self.cmd.stdout.write(''.join([
215 error.__class__.__name__, ': ', str(error), '\n']))
216 self.cmd.stdout.write(prompt_string)
217 value = parser(msg, self.cmd.stdin.readline())
219 response = msg.response(value)
221 except ValueError, error:
223 self.cmd.inqueue.put(response)
225 def _boolean_request_prompt(self, msg):
226 if msg.default == True:
232 def _boolean_request_parser(self, msg, response):
233 value = response.strip().lower()
234 if value.startswith('y'):
236 elif value.startswith('n'):
238 elif len(value) == 0:
242 def _string_request_prompt(self, msg):
243 if msg.default == None:
246 d = ' [%s] ' % msg.default
249 def _string_request_parser(self, msg, response):
250 response = response.strip()
253 return response.strip()
255 def _float_request_prompt(self, msg):
256 return self._string_request_prompt(msg)
258 def _float_request_parser(self, msg, resposne):
259 if response.strip() == '':
261 return float(response)
263 def _selection_request_prompt(self, msg):
265 for i,option in enumerate(msg.options):
266 options.append(' %d) %s' % (i,option))
267 options = ''.join(options)
268 if msg.default == None:
271 prompt = '? [%d] ' % msg.default
272 return '\n'.join([msg.msg,options,prompt])
274 def _selection_request_parser(self, msg, response):
275 if response.strip() == '':
279 def _point_request_prompt(self, msg):
280 block = msg.curve.data[msg.block]
281 block_info = ('(curve: %s, block: %s, %d points)'
286 if msg.default == None:
289 prompt = '? [%d] ' % msg.default
290 return ' '.join([msg.msg,block_info,prompt])
292 def _point_request_parser(self, msg, response):
293 if response.strip() == '':
298 class HelpCommand (CommandMethod):
299 def __init__(self, *args, **kwargs):
300 super(HelpCommand, self).__init__(*args, **kwargs)
301 self.parser = CommandLineParser(self.command, self.name_fn)
304 blocks = [self.command.help(name_fn=self.name_fn),
306 'Usage: ' + self._usage_string(),
308 self.cmd.stdout.write('\n'.join(blocks))
311 return self.command.help(name_fn=self.name_fn)
313 def _usage_string(self):
314 if len(self.parser.command_opts) == 0:
317 options_string = '[options]'
318 arg_string = ' '.join(
319 [self.name_fn(arg.name) for arg in self.parser.command_args])
320 return ' '.join([x for x in [self.parser.prog,
325 class CompleteCommand (CommandMethod):
326 def __call__(self, text, line, begidx, endidx):
330 # Define some additional commands
332 class LocalHelpCommand (Command):
333 """Called with an argument, prints that command's documentation.
335 With no argument, lists all available help topics as well as any
336 undocumented commands.
339 super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
340 # We set .arguments now (vs. using th arguments option to __init__),
341 # to overwrite the default help argument. We don't override
342 # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
344 Argument(name='command', type='string', optional=True,
345 help='The name of the command you want help with.')
348 def _run(self, hooke, inqueue, outqueue, params):
349 raise NotImplementedError # cmd.Cmd already implements .do_help()
351 class LocalExitCommand (Command):
352 """Exit Hooke cleanly.
355 super(LocalExitCommand, self).__init__(
356 name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
358 Argument(name='force', type='bool', default=False,
360 Exit without prompting the user. Use if you save often or don't make
365 def _run(self, hooke, inqueue, outqueue, params):
366 """The guts of the `do_exit/_quit/_EOF` commands.
368 A `True` return stops :meth:`.cmdloop` execution.
371 if params['force'] == False:
372 not_saved = [p.name for p in hooke.playlists
373 if p.is_saved() == False]
376 if len(not_saved) > 0:
377 msg = 'Unsaved playlists (%s). %s' \
378 % (', '.join([str(p) for p in not_saved]), msg)
380 outqueue.put(BooleanRequest(msg, default))
381 result = inqueue.get()
382 assert result.type == 'boolean'
388 # Now onto the main attraction.
390 class HookeCmd (cmd.Cmd):
391 def __init__(self, ui, commands, inqueue, outqueue):
392 cmd.Cmd.__init__(self)
394 self.commands = commands
395 self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
396 self.prompt = 'hooke> '
397 self._add_command_methods()
398 self.inqueue = inqueue
399 self.outqueue = outqueue
401 def _name_fn(self, name):
402 return name.replace(' ', '_')
404 def _add_command_methods(self):
405 for command in self.commands + self.local_commands:
406 for name in [command.name] + command.aliases:
407 name = self._name_fn(name)
408 setattr(self.__class__, 'help_%s' % name,
409 HelpCommand(self, command, self._name_fn))
411 setattr(self.__class__, 'do_%s' % name,
412 DoCommand(self, command, self._name_fn))
413 setattr(self.__class__, 'complete_%s' % name,
414 CompleteCommand(self, command, self._name_fn))
416 def parseline(self, line):
417 """Override Cmd.parseline to use shlex.split.
421 This allows us to handle comments cleanly. With the default
422 Cmd implementation, a pure comment line will call the .default
425 Since we use shlex to strip comments, we return a list of
426 split arguments rather than the raw argument string.
429 argv = shlex.split(line, comments=True, posix=True)
431 return None, None, '' # return an empty line
438 return cmd, args, line
440 def do_help(self, arg):
441 """Wrap Cmd.do_help to handle our .parseline argument list.
444 return cmd.Cmd.do_help(self, '')
445 return cmd.Cmd.do_help(self, arg[0])
448 """Override Cmd.emptyline to not do anything.
450 Repeating the last non-empty command seems unwise. Explicit
451 is better than implicit.
456 class CommandLine (UserInterface):
457 """Command line interface. Simple and powerful.
460 super(CommandLine, self).__init__(name='command line')
462 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
463 cmd = HookeCmd(self, commands,
464 inqueue=ui_to_command_queue,
465 outqueue=command_to_ui_queue)
466 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
467 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
470 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
471 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
472 cmd.cmdloop(self._splash_text(extra_info={
473 'get-details':'run `license`',
476 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
478 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)