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
26 import readline # including readline makes cmd.Cmd.cmdloop() smarter
29 from ..command import CommandExit, Exit, Command, Argument, StoreValue
30 from ..interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
31 from ..ui import UserInterface, CommandMessage
32 from ..util.convert import from_string
33 from ..util.encoding import get_input_encoding, get_output_encoding
36 # Define a few helper classes.
38 class Default (object):
39 """Marker for options not given on the command line.
43 class CommandLineParser (optparse.OptionParser):
44 """Implement a command line syntax for a
45 :class:`hooke.command.Command`.
47 def __init__(self, command, name_fn):
48 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
49 self.command = command
50 self.command_opts = []
51 self.command_args = []
52 for a in command.arguments:
54 continue # 'help' is a default OptionParser option
55 if a.optional == True:
56 name = name_fn(a.name)
61 '--disable-%s' % name, dest=name, default=Default,
63 self.command_opts.append(a)
65 elif a.default == False:
67 '--enable-%s' % name, dest=name, default=Default,
69 self.command_opts.append(a)
73 elif type not in ['string', 'int', 'long', 'choice', 'float',
77 '--%s' % name, dest=name, type=type, default=Default)
78 self.command_opts.append(a)
80 self.command_args.append(a)
81 infinite_counters = [a for a in self.command_args if a.count == -1]
82 assert len(infinite_counters) <= 1, \
83 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
84 % (command.name, ', '.join([a.name for a in infinite_counters]))
85 if len(infinite_counters) == 1: # move the big counter to the end.
86 infinite_counter = infinite_counters[0]
87 self.command_args.remove(infinite_counter)
88 self.command_args.append(infinite_counter)
90 def exit(self, status=0, msg=None):
91 """Override :meth:`optparse.OptionParser.exit` which calls
95 raise optparse.OptParseError(msg)
96 raise optparse.OptParseError('OptParse EXIT')
98 class CommandMethod (object):
99 """Base class for method replacer.
101 The .__call__ methods of `CommandMethod` subclasses functions will
102 provide the `do_*`, `help_*`, and `complete_*` methods of
105 def __init__(self, cmd, command, name_fn):
107 self.command = command
108 self.name_fn = name_fn
110 def __call__(self, *args, **kwargs):
111 raise NotImplementedError
113 class DoCommand (CommandMethod):
114 def __init__(self, *args, **kwargs):
115 super(DoCommand, self).__init__(*args, **kwargs)
116 self.parser = CommandLineParser(self.command, self.name_fn)
118 def __call__(self, args):
120 args = self._parse_args(args)
121 except optparse.OptParseError, e:
122 self.cmd.stdout.write(str(e).lstrip()+'\n')
123 self.cmd.stdout.write('Failure\n')
126 self.cmd.inqueue.put(CommandMessage(self.command, args))
128 msg = self.cmd.outqueue.get()
129 if isinstance(msg, Exit):
131 elif isinstance(msg, CommandExit):
132 self.cmd.stdout.write(msg.__class__.__name__+'\n')
133 self.cmd.stdout.write(str(msg).rstrip()+'\n')
135 elif isinstance(msg, ReloadUserInterfaceConfig):
136 self.cmd.ui.reload_config(msg.config)
138 elif isinstance(msg, Request):
139 self._handle_request(msg)
141 self.cmd.stdout.write(str(msg).rstrip()+'\n')
143 def _parse_args(self, args):
144 options,args = self.parser.parse_args(args)
145 self._check_argument_length_bounds(args)
147 for argument in self.parser.command_opts:
148 value = getattr(options, self.name_fn(argument.name))
150 params[argument.name] = value
152 for argument in self.parser.command_args:
153 if argument.count == 1:
154 params[argument.name] = from_string(args[arg_index],
156 elif argument.count > 1:
157 params[argument.name] = [
158 from_string(a, argument.type)
159 for a in args[arg_index:arg_index+argument.count]]
160 else: # argument.count == -1:
161 params[argument.name] = [
162 from_string(a, argument.type) for a in args[arg_index:]]
163 arg_index += argument.count
166 def _check_argument_length_bounds(self, arguments):
167 """Check that there are an appropriate number of arguments in
170 If not, raise optparse.OptParseError().
174 for argument in self.parser.command_args:
175 if argument.optional == False and argument.count > 0:
176 min_args += argument.count
177 if max_args >= 0: # otherwise already infinite
178 if argument.count == -1:
181 max_args += argument.count
182 if len(arguments) < min_args \
183 or (max_args >= 0 and len(arguments) > max_args):
184 if min_args == max_args:
185 target_string = str(min_args)
187 target_string = 'more than %d' % min_args
189 target_string = '%d to %d' % (min_args, max_args)
190 raise optparse.OptParseError(
191 '%d arguments given, but %s takes %s'
192 % (len(arguments), self.name_fn(self.command.name),
195 def _handle_request(self, msg):
196 """Repeatedly try to get a response to `msg`.
198 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
200 raise NotImplementedError('_%s_request_prompt' % msg.type)
201 prompt_string = prompt(msg)
202 parser = getattr(self, '_%s_request_parser' % msg.type, None)
204 raise NotImplementedError('_%s_request_parser' % msg.type)
208 self.cmd.stdout.write(''.join([
209 error.__class__.__name__, ': ', str(error), '\n']))
210 self.cmd.stdout.write(prompt_string)
211 value = parser(msg, self.cmd.stdin.readline())
213 response = msg.response(value)
215 except ValueError, error:
217 self.cmd.inqueue.put(response)
219 def _boolean_request_prompt(self, msg):
220 if msg.default == True:
226 def _boolean_request_parser(self, msg, response):
227 value = response.strip().lower()
228 if value.startswith('y'):
230 elif value.startswith('n'):
232 elif len(value) == 0:
236 def _string_request_prompt(self, msg):
237 if msg.default == None:
240 d = ' [%s] ' % msg.default
243 def _string_request_parser(self, msg, response):
244 response = response.strip()
247 return response.strip()
249 def _float_request_prompt(self, msg):
250 return self._string_request_prompt(msg)
252 def _float_request_parser(self, msg, resposne):
253 if response.strip() == '':
255 return float(response)
257 def _selection_request_prompt(self, msg):
259 for i,option in enumerate(msg.options):
260 options.append(' %d) %s' % (i,option))
261 options = ''.join(options)
262 if msg.default == None:
265 prompt = '? [%d] ' % msg.default
266 return '\n'.join([msg.msg,options,prompt])
268 def _selection_request_parser(self, msg, response):
269 if response.strip() == '':
273 def _point_request_prompt(self, msg):
274 block = msg.curve.data[msg.block]
275 block_info = ('(curve: %s, block: %s, %d points)'
280 if msg.default == None:
283 prompt = '? [%d] ' % msg.default
284 return ' '.join([msg.msg,block_info,prompt])
286 def _point_request_parser(self, msg, response):
287 if response.strip() == '':
292 class HelpCommand (CommandMethod):
293 def __init__(self, *args, **kwargs):
294 super(HelpCommand, self).__init__(*args, **kwargs)
295 self.parser = CommandLineParser(self.command, self.name_fn)
298 blocks = [self.command.help(name_fn=self.name_fn),
300 'Usage: ' + self._usage_string(),
302 self.cmd.stdout.write('\n'.join(blocks))
305 return self.command.help(name_fn=self.name_fn)
307 def _usage_string(self):
308 if len(self.parser.command_opts) == 0:
311 options_string = '[options]'
312 arg_string = ' '.join(
313 [self.name_fn(arg.name) for arg in self.parser.command_args])
314 return ' '.join([x for x in [self.parser.prog,
319 class CompleteCommand (CommandMethod):
320 def __call__(self, text, line, begidx, endidx):
324 # Define some additional commands
326 class LocalHelpCommand (Command):
327 """Called with an argument, prints that command's documentation.
329 With no argument, lists all available help topics as well as any
330 undocumented commands.
333 super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
334 # We set .arguments now (vs. using th arguments option to __init__),
335 # to overwrite the default help argument. We don't override
336 # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
338 Argument(name='command', type='string', optional=True,
339 help='The name of the command you want help with.')
342 def _run(self, hooke, inqueue, outqueue, params):
343 raise NotImplementedError # cmd.Cmd already implements .do_help()
345 class LocalExitCommand (Command):
346 """Exit Hooke cleanly.
349 super(LocalExitCommand, self).__init__(
350 name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
352 Argument(name='force', type='bool', default=False,
354 Exit without prompting the user. Use if you save often or don't make
359 def _run(self, hooke, inqueue, outqueue, params):
360 """The guts of the `do_exit/_quit/_EOF` commands.
362 A `True` return stops :meth:`.cmdloop` execution.
365 if params['force'] == False:
366 not_saved = [p.name for p in hooke.playlists
367 if p.is_saved() == False]
370 if len(not_saved) > 0:
371 msg = 'Unsaved playlists (%s). %s' \
372 % (', '.join([str(p) for p in not_saved]), msg)
374 outqueue.put(BooleanRequest(msg, default))
375 result = inqueue.get()
376 assert result.type == 'boolean'
382 # Now onto the main attraction.
384 class HookeCmd (cmd.Cmd):
385 def __init__(self, ui, commands, inqueue, outqueue):
386 cmd.Cmd.__init__(self)
388 self.commands = commands
389 self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
390 self.prompt = 'hooke> '
391 self._add_command_methods()
392 self.inqueue = inqueue
393 self.outqueue = outqueue
395 def _name_fn(self, name):
396 return name.replace(' ', '_')
398 def _add_command_methods(self):
399 for command in self.commands + self.local_commands:
400 for name in [command.name] + command.aliases:
401 name = self._name_fn(name)
402 setattr(self.__class__, 'help_%s' % name,
403 HelpCommand(self, command, self._name_fn))
405 setattr(self.__class__, 'do_%s' % name,
406 DoCommand(self, command, self._name_fn))
407 setattr(self.__class__, 'complete_%s' % name,
408 CompleteCommand(self, command, self._name_fn))
410 def parseline(self, line):
411 """Override Cmd.parseline to use shlex.split.
415 This allows us to handle comments cleanly. With the default
416 Cmd implementation, a pure comment line will call the .default
419 Since we use shlex to strip comments, we return a list of
420 split arguments rather than the raw argument string.
423 argv = shlex.split(line, comments=True, posix=True)
425 return None, None, '' # return an empty line
430 return argv[0], argv[1:], line
432 def do_help(self, arg):
433 """Wrap Cmd.do_help to handle our .parseline argument list.
436 return cmd.Cmd.do_help(self, '')
437 return cmd.Cmd.do_help(self, arg[0])
440 """Override Cmd.emptyline to not do anything.
442 Repeating the last non-empty command seems unwise. Explicit
443 is better than implicit.
447 class CommandLine (UserInterface):
448 """Command line interface. Simple and powerful.
451 super(CommandLine, self).__init__(name='command line')
453 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
454 cmd = HookeCmd(self, commands,
455 inqueue=ui_to_command_queue,
456 outqueue=command_to_ui_queue)
457 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
458 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
461 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
462 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
463 cmd.cmdloop(self._splash_text(extra_info={
464 'get-details':'run `license`',
467 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
469 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)