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.encoding import get_input_encoding, get_output_encoding
35 # Define a few helper classes.
37 class Default (object):
38 """Marker for options not given on the command line.
42 class CommandLineParser (optparse.OptionParser):
43 """Implement a command line syntax for a
44 :class:`hooke.command.Command`.
46 def __init__(self, command, name_fn):
47 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
48 self.command = command
49 self.command_opts = []
50 self.command_args = []
51 for a in command.arguments:
53 continue # 'help' is a default OptionParser option
54 if a.optional == True:
55 name = name_fn(a.name)
57 '--%s' % name, dest=name, default=Default)
58 self.command_opts.append(a)
60 self.command_args.append(a)
61 infinite_counters = [a for a in self.command_args if a.count == -1]
62 assert len(infinite_counters) <= 1, \
63 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
64 % (command.name, ', '.join([a.name for a in infinite_counters]))
65 if len(infinite_counters) == 1: # move the big counter to the end.
66 infinite_counter = infinite_counters[0]
67 self.command_args.remove(infinite_counter)
68 self.command_args.append(infinite_counter)
70 def exit(self, status=0, msg=None):
71 """Override :meth:`optparse.OptionParser.exit` which calls
75 raise optparse.OptParseError(msg)
76 raise optparse.OptParseError('OptParse EXIT')
78 class CommandMethod (object):
79 """Base class for method replacer.
81 The .__call__ methods of `CommandMethod` subclasses functions will
82 provide the `do_*`, `help_*`, and `complete_*` methods of
85 def __init__(self, cmd, command, name_fn):
87 self.command = command
88 self.name_fn = name_fn
90 def __call__(self, *args, **kwargs):
91 raise NotImplementedError
93 class DoCommand (CommandMethod):
94 def __init__(self, *args, **kwargs):
95 super(DoCommand, self).__init__(*args, **kwargs)
96 self.parser = CommandLineParser(self.command, self.name_fn)
98 def __call__(self, args):
100 args = self._parse_args(args)
101 except optparse.OptParseError, e:
102 self.cmd.stdout.write(str(e).lstrip()+'\n')
103 self.cmd.stdout.write('Failure\n')
105 self.cmd.inqueue.put(CommandMessage(self.command, args))
107 msg = self.cmd.outqueue.get()
108 if isinstance(msg, Exit):
110 elif isinstance(msg, CommandExit):
111 self.cmd.stdout.write(msg.__class__.__name__+'\n')
112 self.cmd.stdout.write(str(msg).rstrip()+'\n')
114 elif isinstance(msg, ReloadUserInterfaceConfig):
115 self.cmd.ui.reload_config(msg.config)
117 elif isinstance(msg, Request):
118 self._handle_request(msg)
120 self.cmd.stdout.write(str(msg).rstrip()+'\n')
122 def _parse_args(self, args):
123 options,args = self.parser.parse_args(args)
124 self._check_argument_length_bounds(args)
126 for argument in self.parser.command_opts:
127 value = getattr(options, self.name_fn(argument.name))
129 params[argument.name] = value
131 for argument in self.parser.command_args:
132 if argument.count == 1:
133 params[argument.name] = args[arg_index]
134 elif argument.count > 1:
135 params[argument.name] = \
136 args[arg_index:arg_index+argument.count]
137 else: # argument.count == -1:
138 params[argument.name] = args[arg_index:]
139 arg_index += argument.count
142 def _check_argument_length_bounds(self, arguments):
143 """Check that there are an appropriate number of arguments in
146 If not, raise optparse.OptParseError().
150 for argument in self.parser.command_args:
151 if argument.optional == False and argument.count > 0:
152 min_args += argument.count
153 if max_args >= 0: # otherwise already infinite
154 if argument.count == -1:
157 max_args += argument.count
158 if len(arguments) < min_args \
159 or (max_args >= 0 and len(arguments) > max_args):
160 if min_args == max_args:
161 target_string = str(min_args)
163 target_string = 'more than %d' % min_args
165 target_string = '%d to %d' % (min_args, max_args)
166 raise optparse.OptParseError(
167 '%d arguments given, but %s takes %s'
168 % (len(arguments), self.name_fn(self.command.name),
171 def _handle_request(self, msg):
172 """Repeatedly try to get a response to `msg`.
174 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
176 raise NotImplementedError('_%s_request_prompt' % msg.type)
177 prompt_string = prompt(msg)
178 parser = getattr(self, '_%s_request_parser' % msg.type, None)
180 raise NotImplementedError('_%s_request_parser' % msg.type)
184 self.cmd.stdout.write(''.join([
185 error.__class__.__name__, ': ', str(error), '\n']))
186 self.cmd.stdout.write(prompt_string)
187 value = parser(msg, self.cmd.stdin.readline())
189 response = msg.response(value)
191 except ValueError, error:
193 self.cmd.inqueue.put(response)
195 def _boolean_request_prompt(self, msg):
196 if msg.default == True:
202 def _boolean_request_parser(self, msg, response):
203 value = response.strip().lower()
204 if value.startswith('y'):
206 elif value.startswith('n'):
208 elif len(value) == 0:
212 def _string_request_prompt(self, msg):
213 if msg.default == None:
216 d = ' [%s] ' % msg.default
219 def _string_request_parser(self, msg, response):
220 response = response.strip()
223 return response.strip()
225 def _float_request_prompt(self, msg):
226 return self._string_request_prompt(msg)
228 def _float_request_parser(self, msg, resposne):
229 if response.strip() == '':
231 return float(response)
233 def _selection_request_prompt(self, msg):
235 for i,option in enumerate(msg.options):
236 options.append(' %d) %s' % (i,option))
237 options = ''.join(options)
238 if msg.default == None:
241 prompt = '? [%d] ' % msg.default
242 return '\n'.join([msg.msg,options,prompt])
244 def _selection_request_parser(self, msg, response):
245 if response.strip() == '':
249 def _point_request_prompt(self, msg):
250 block = msg.curve.data[msg.block]
251 block_info = ('(curve: %s, block: %s, %d points)'
256 if msg.default == None:
259 prompt = '? [%d] ' % msg.default
260 return ' '.join([msg.msg,block_info,prompt])
262 def _point_request_parser(self, msg, response):
263 if response.strip() == '':
268 class HelpCommand (CommandMethod):
269 def __init__(self, *args, **kwargs):
270 super(HelpCommand, self).__init__(*args, **kwargs)
271 self.parser = CommandLineParser(self.command, self.name_fn)
274 blocks = [self.command.help(name_fn=self.name_fn),
276 'Usage: ' + self._usage_string(),
278 self.cmd.stdout.write('\n'.join(blocks))
281 return self.command.help(name_fn=self.name_fn)
283 def _usage_string(self):
284 if len(self.parser.command_opts) == 0:
287 options_string = '[options]'
288 arg_string = ' '.join(
289 [self.name_fn(arg.name) for arg in self.parser.command_args])
290 return ' '.join([x for x in [self.parser.prog,
295 class CompleteCommand (CommandMethod):
296 def __call__(self, text, line, begidx, endidx):
300 # Define some additional commands
302 class LocalHelpCommand (Command):
303 """Called with an argument, prints that command's documentation.
305 With no argument, lists all available help topics as well as any
306 undocumented commands.
309 super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
310 # We set .arguments now (vs. using th arguments option to __init__),
311 # to overwrite the default help argument. We don't override
312 # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
314 Argument(name='command', type='string', optional=True,
315 help='The name of the command you want help with.')
318 def _run(self, hooke, inqueue, outqueue, params):
319 raise NotImplementedError # cmd.Cmd already implements .do_help()
321 class LocalExitCommand (Command):
322 """Exit Hooke cleanly.
325 super(LocalExitCommand, self).__init__(
326 name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
328 Argument(name='force', type='bool', default=False,
330 Exit without prompting the user. Use if you save often or don't make
335 def _run(self, hooke, inqueue, outqueue, params):
336 """The guts of the `do_exit/_quit/_EOF` commands.
338 A `True` return stops :meth:`.cmdloop` execution.
341 if params['force'] == False:
342 not_saved = [p.name for p in hooke.playlists
343 if p.is_saved() == False]
346 if len(not_saved) > 0:
347 msg = 'Unsaved playlists (%s). %s' \
348 % (', '.join([str(p) for p in not_saved]), msg)
350 outqueue.put(BooleanRequest(msg, default))
351 result = inqueue.get()
352 assert result.type == 'boolean'
358 # Now onto the main attraction.
360 class HookeCmd (cmd.Cmd):
361 def __init__(self, ui, commands, inqueue, outqueue):
362 cmd.Cmd.__init__(self)
364 self.commands = commands
365 self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
366 self.prompt = 'hooke> '
367 self._add_command_methods()
368 self.inqueue = inqueue
369 self.outqueue = outqueue
371 def _name_fn(self, name):
372 return name.replace(' ', '_')
374 def _add_command_methods(self):
375 for command in self.commands + self.local_commands:
376 for name in [command.name] + command.aliases:
377 name = self._name_fn(name)
378 setattr(self.__class__, 'help_%s' % name,
379 HelpCommand(self, command, self._name_fn))
381 setattr(self.__class__, 'do_%s' % name,
382 DoCommand(self, command, self._name_fn))
383 setattr(self.__class__, 'complete_%s' % name,
384 CompleteCommand(self, command, self._name_fn))
386 def parseline(self, line):
387 """Override Cmd.parseline to use shlex.split.
391 This allows us to handle comments cleanly. With the default
392 Cmd implementation, a pure comment line will call the .default
395 Since we use shlex to strip comments, we return a list of
396 split arguments rather than the raw argument string.
399 argv = shlex.split(line, comments=True, posix=True)
401 return None, None, '' # return an empty line
406 return argv[0], argv[1:], line
408 def do_help(self, arg):
409 """Wrap Cmd.do_help to handle our .parseline argument list.
412 return cmd.Cmd.do_help(self, '')
413 return cmd.Cmd.do_help(self, arg[0])
416 """Override Cmd.emptyline to not do anything.
418 Repeating the last non-empty command seems unwise. Explicit
419 is better than implicit.
423 class CommandLine (UserInterface):
424 """Command line interface. Simple and powerful.
427 super(CommandLine, self).__init__(name='command line')
429 def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
430 cmd = HookeCmd(self, commands,
431 inqueue=ui_to_command_queue,
432 outqueue=command_to_ui_queue)
433 #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
434 cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
437 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
438 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
439 cmd.cmdloop(self._splash_text(extra_info={
440 'get-details':'run `license`',
443 def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
445 cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)