1 """Defines :class:`CommandLine` for driving Hooke from the command
7 import readline # including readline makes cmd.Cmd.cmdloop() smarter
10 from ..command import CommandExit, Exit, Command, Argument, StoreValue
11 from ..interaction import Request, BooleanRequest
12 from ..ui import UserInterface, CommandMessage
15 # Define a few helper classes.
17 class Default (object):
18 """Marker for options not given on the command line.
22 class CommandLineParser (optparse.OptionParser):
23 """Implement a command line syntax for a
24 :class:`hooke.command.Command`.
26 def __init__(self, command, name_fn):
27 optparse.OptionParser.__init__(self, prog=name_fn(command.name))
28 self.command = command
29 self.command_opts = []
30 self.command_args = []
31 for a in command.arguments:
33 continue # 'help' is a default OptionParser option
34 if a.optional == True:
35 name = name_fn(a.name)
37 '--%s' % name, dest=name, default=Default)
38 self.command_opts.append(a)
40 self.command_args.append(a)
41 infinite_counters = [a for a in self.command_args if a.count == -1]
42 assert len(infinite_counters) <= 1, \
43 'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
44 % (command.name, ', '.join([a.name for a in infinite_counters]))
45 if len(infinite_counters) == 1: # move the big counter to the end.
46 infinite_counter = infinite_counters[0]
47 self.command_args.remove(infinite_counter)
48 self.command_args.append(infinite_counter)
50 def exit(self, status=0, msg=None):
51 """Override :meth:`optparse.OptionParser.exit` which calls
55 raise optparse.OptParseError(msg)
56 raise optparse.OptParseError('OptParse EXIT')
58 class CommandMethod (object):
59 """Base class for method replacer.
61 The .__call__ methods of `CommandMethod` subclasses functions will
62 provide the `do_*`, `help_*`, and `complete_*` methods of
65 def __init__(self, cmd, command, name_fn):
67 self.command = command
68 self.name_fn = name_fn
70 def __call__(self, *args, **kwargs):
71 raise NotImplementedError
73 class DoCommand (CommandMethod):
74 def __init__(self, *args, **kwargs):
75 super(DoCommand, self).__init__(*args, **kwargs)
76 self.parser = CommandLineParser(self.command, self.name_fn)
78 def __call__(self, args):
80 args = self._parse_args(args)
81 except optparse.OptParseError, e:
82 self.cmd.stdout.write(str(e).lstrip()+'\n')
83 self.cmd.stdout.write('Failure\n')
85 self.cmd.inqueue.put(CommandMessage(self.command, args))
87 msg = self.cmd.outqueue.get()
88 if isinstance(msg, Exit):
90 elif isinstance(msg, CommandExit):
91 self.cmd.stdout.write(msg.__class__.__name__+'\n')
92 self.cmd.stdout.write(str(msg).rstrip()+'\n')
94 elif isinstance(msg, Request):
95 self._handle_request(msg)
97 self.cmd.stdout.write(str(msg).rstrip()+'\n')
99 def _parse_args(self, args):
100 argv = shlex.split(args, comments=True, posix=True)
101 options,args = self.parser.parse_args(argv)
102 self._check_argument_length_bounds(args)
104 for argument in self.parser.command_opts:
105 value = getattr(options, self.name_fn(argument.name))
107 params[argument.name] = value
109 for argument in self.parser.command_args:
110 if argument.count == 1:
111 params[argument.name] = args[arg_index]
112 elif argument.count > 1:
113 params[argument.name] = \
114 args[arg_index:arg_index+argument.count]
115 else: # argument.count == -1:
116 params[argument.name] = args[arg_index:]
117 arg_index += argument.count
120 def _check_argument_length_bounds(self, arguments):
121 """Check that there are an appropriate number of arguments in
124 If not, raise optparse.OptParseError().
128 for argument in self.parser.command_args:
129 if argument.optional == False and argument.count > 0:
130 min_args += argument.count
131 if max_args >= 0: # otherwise already infinite
132 if argument.count == -1:
135 max_args += argument.count
136 if len(arguments) < min_args \
137 or (max_args >= 0 and len(arguments) > max_args):
138 if min_args == max_args:
139 target_string = str(min_args)
141 target_string = 'more than %d' % min_args
143 target_string = '%d to %d' % (min_args, max_args)
144 raise optparse.OptParseError(
145 '%d arguments given, but %s takes %s'
146 % (len(arguments), self.name_fn(self.command.name),
149 def _handle_request(self, msg):
150 """Repeatedly try to get a response to `msg`.
152 prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
154 raise NotImplementedError('_%s_request_prompt' % msg.type)
155 prompt_string = prompt(msg)
156 parser = getattr(self, '_%s_request_parser' % msg.type, None)
158 raise NotImplementedError('_%s_request_parser' % msg.type)
162 self.cmd.stdout.write(''.join([
163 error.__class__.__name__, ': ', str(error), '\n']))
164 self.cmd.stdout.write(prompt_string)
165 value = parser(msg, self.cmd.stdin.readline())
167 response = msg.response(value)
169 except ValueError, error:
171 self.cmd.inqueue.put(response)
173 def _boolean_request_prompt(self, msg):
174 if msg.default == True:
180 def _boolean_request_parser(self, msg, response):
181 value = response.strip().lower()
182 if value.startswith('y'):
184 elif value.startswith('n'):
186 elif len(value) == 0:
190 def _string_request_prompt(self, msg):
191 if msg.default == None:
194 d = ' [%s] ' % msg.default
197 def _string_request_parser(self, msg, response):
198 return response.strip()
200 def _float_request_prompt(self, msg):
201 return self._string_request_prompt(msg)
203 def _float_request_parser(self, msg, resposne):
204 return float(response)
206 def _selection_request_prompt(self, msg):
208 for i,option in enumerate(msg.options):
209 options.append(' %d) %s' % (i,option))
210 options = ''.join(options)
211 if msg.default == None:
214 prompt = '? [%d] ' % msg.default
215 return '\n'.join([msg,options,prompt])
217 def _selection_request_parser(self, msg, response):
221 class HelpCommand (CommandMethod):
222 def __init__(self, *args, **kwargs):
223 super(HelpCommand, self).__init__(*args, **kwargs)
224 self.parser = CommandLineParser(self.command, self.name_fn)
227 blocks = [self.command.help(name_fn=self.name_fn),
229 'Usage: ' + self._usage_string(),
231 self.cmd.stdout.write('\n'.join(blocks))
234 return self.command.help(name_fn=self.name_fn)
236 def _usage_string(self):
237 if len(self.parser.command_opts) == 0:
240 options_string = '[options]'
241 arg_string = ' '.join(
242 [self.name_fn(arg.name) for arg in self.parser.command_args])
243 return ' '.join([x for x in [self.parser.prog,
248 class CompleteCommand (CommandMethod):
249 def __call__(self, text, line, begidx, endidx):
253 # Define some additional commands
255 class LocalHelpCommand (Command):
256 """Called with an argument, prints that command's documentation.
258 With no argument, lists all available help topics as well as any
259 undocumented commands.
262 super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
263 # We set .arguments now (vs. using th arguments option to __init__),
264 # to overwrite the default help argument. We don't override
265 # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
267 Argument(name='command', type='string', optional=True,
268 help='The name of the command you want help with.')
271 def _run(self, hooke, inqueue, outqueue, params):
272 raise NotImplementedError # cmd.Cmd already implements .do_help()
274 class LocalExitCommand (Command):
275 """Exit Hooke cleanly.
278 super(LocalExitCommand, self).__init__(
279 name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
281 Argument(name='force', type='bool', default=False,
282 callback=StoreValue(True), help="""
283 Exit without prompting the user. Use if you save often or don't make
288 def _run(self, hooke, inqueue, outqueue, params):
289 """The guts of the `do_exit/_quit/_EOF` commands.
291 A `True` return stops :meth:`.cmdloop` execution.
294 if params['force'] == False:
295 # TODO: get results of hooke.playlists.current().is_saved()
299 if is_saved == False:
300 msg = 'You did not save your playlist. ' + msg
302 outqueue.put(BooleanRequest(msg, default))
303 result = inqueue.get()
304 assert result.type == 'boolean'
310 # Now onto the main attraction.
312 class HookeCmd (cmd.Cmd):
313 def __init__(self, commands, inqueue, outqueue):
314 cmd.Cmd.__init__(self)
315 self.commands = commands
316 self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
317 self.prompt = 'hooke> '
318 self._add_command_methods()
319 self.inqueue = inqueue
320 self.outqueue = outqueue
322 def _name_fn(self, name):
323 return name.replace(' ', '_')
325 def _add_command_methods(self):
326 for command in self.commands + self.local_commands:
327 for name in [command.name] + command.aliases:
328 name = self._name_fn(name)
329 setattr(self.__class__, 'help_%s' % name,
330 HelpCommand(self, command, self._name_fn))
332 setattr(self.__class__, 'do_%s' % name,
333 DoCommand(self, command, self._name_fn))
334 setattr(self.__class__, 'complete_%s' % name,
335 CompleteCommand(self, command, self._name_fn))
338 class CommandLine (UserInterface):
339 """Command line interface. Simple and powerful.
342 super(CommandLine, self).__init__(name='command line')
344 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
345 cmd = HookeCmd(commands,
346 inqueue=ui_to_command_queue,
347 outqueue=command_to_ui_queue)
348 cmd.cmdloop(self._splash_text())