Use ._cl_name and ._cl_aliases instead of overwriting .name and .aliases.
[hooke.git] / hooke / ui / commandline.py
1 """Defines :class:`CommandLine` for driving Hooke from the command
2 line.
3 """
4
5 import cmd
6 import optparse
7 import readline # including readline makes cmd.Cmd.cmdloop() smarter
8 import shlex
9
10 from ..command import CommandExit, Command, Argument
11 from ..ui import UserInterface, CommandMessage
12
13
14 # Define a few helper classes.
15
16 class CommandLineParser (optparse.OptionParser):
17     """Implement a command line syntax for a
18     :class:`hooke.command.Command`.
19     """
20     def __init__(self, command):
21         optparse.OptionParser.__init__(self, prog=command._cl_name)
22         self.command = command
23         self.command_opts = []
24         self.command_args = []
25         for a in command.arguments:
26             if a.name == 'help':
27                 continue # 'help' is a default OptionParser option
28             if a.optional == True:
29                 self.add_option(
30                     '--%s' % a._cl_name, dest=a._cl_name, default=a.default)
31                 self.command_opts.append(a)
32             else:
33                 self.command_args.append(a)
34
35     def exit(self, status=0, msg=None):
36         """Override :meth:`optparse.OptionParser.exit` which calls
37         :func:`sys.exit`.
38         """
39         if msg:
40             raise optparse.OptParseError(msg)
41         raise optparse.OptParseError('OptParse EXIT')
42
43 class CommandMethod (object):
44     """Base class for method replacer.
45
46     The .__call__ methods of `CommandMethod` subclasses functions will
47     provide the `do_*`, `help_*`, and `complete_*` methods of
48     :class:`HookeCmd`.
49     """
50     def __init__(self, cmd, command):
51         self.cmd = cmd
52         self.command = command
53
54     def __call__(self, *args, **kwargs):
55         raise NotImplementedError
56
57 class DoCommand (CommandMethod):
58     def __init__(self, *args, **kwargs):
59         super(DoCommand, self).__init__(*args, **kwargs)
60         self.parser = CommandLineParser(self.command)
61
62     def __call__(self, args):
63         try:
64             args = self._parse_args(self.command, args)
65         except optparse.OptParseError, e:
66             self.cmd.stdout.write(str(e).lstrip()+'\n')
67             self.cmd.stdout.write('Failure\n')
68             return
69         self.cmd.inqueue.put(CommandMessage(self.command, args))
70         while True:
71             msg = self.cmd.outqueue.get()
72             if isinstance(msg, CommandExit):
73                 self.cmd.stdout.write(msg.__class__.__name__+'\n')
74                 self.cmd.stdout.write(str(msg).rstrip()+'\n')
75                 break
76             self.cmd.stdout.write(str(msg).rstrip()+'\n')
77
78     def _parse_args(self, command, args):
79         argv = shlex.split(args, comments=True, posix=True)
80         options,args = self.parser.parse_args(argv)
81         if len(args) != len(self.parser.command_args):
82             raise optparse.OptParseError('%d arguments given, but %s takes %d'
83                                          % (len(args), command._cl_name,
84                                             len(self.parser.command_args)))
85         params = {}
86         for argument in self.parser.command_opts:
87             params[argument.name] = getattr(options, argument._cl_name)
88         for i,argument in enumerate(self.parser.command_args):
89             params[argument.name] = args[i]
90         return params
91
92 class HelpCommand (CommandMethod):
93     def __init__(self, *args, **kwargs):
94         super(HelpCommand, self).__init__(*args, **kwargs)
95         self.parser = CommandLineParser(self.command)
96
97     def __call__(self):
98         blocks = [self.command.help(),
99                   '------',
100                   'Usage: ' + self._usage_string(),
101                   '']
102         self.cmd.stdout.write('\n'.join(blocks))
103
104     def _message(self):
105         return self.command.help()
106
107     def _usage_string(self):
108         if len(self.parser.command_opts) == 0:
109             options_string = ''
110         else:
111             options_string = '[options]'
112         arg_string = ' '.join(
113             [arg._cl_name for arg in self.parser.command_args])
114         return ' '.join([x for x in [self.command.name,
115                                      options_string,
116                                      arg_string]
117                          if x != ''])
118
119 class CompleteCommand (CommandMethod):
120     def __call__(self, text, line, begidx, endidx):
121         pass
122
123
124 # Now onto the main attraction.
125
126 class HookeCmd (cmd.Cmd):
127     def __init__(self, commands, inqueue, outqueue):
128         cmd.Cmd.__init__(self)
129         self.commands = commands
130         self.prompt = 'hooke> '
131         self._add_command_methods()
132         self.inqueue = inqueue
133         self.outqueue = outqueue
134
135     def _safe_name(self, name):
136         return name.lower().replace(' ', '_')
137
138     def _add_command_methods(self):
139         for command in self.commands:
140             self._setup_command(command)
141             for name in [command._cl_name] + command._cl_aliases:
142                 setattr(self.__class__, 'do_%s' % name,
143                         DoCommand(self, command))
144                 setattr(self.__class__, 'help_%s' % name,
145                         HelpCommand(self, command))
146                 setattr(self.__class__, 'complete_%s' % name,
147                         CompleteCommand(self, command))
148
149         exit_command = Command(
150             name='exit', aliases=['quit', 'EOF'],
151             help='Exit Hooke cleanly.')
152         self._setup_command(exit_command)
153         exit_command.arguments = [] # remove help argument
154         for name in [exit_command.name] + exit_command.aliases:
155             setattr(self.__class__, 'do_%s' % name,
156                     lambda self, args : True)
157             # the True return stops .cmdloop execution
158             setattr(self.__class__, 'help_%s' % name,
159                     HelpCommand(self, exit_command))
160
161         help_command = Command(
162             name='help',
163             help="""
164 Called with an argument, prints that command's documentation.
165
166 With no argument, lists all available help topics as well as any
167 undocumented commands.
168 """.strip())
169         help_command.arguments = [ # overwrite help argument
170             Argument(name='command', type='string', optional=True,
171                      help='The name of the command you want help with.')
172             ]
173         self._setup_command(help_command)
174         setattr(self.__class__, 'help_help',
175                 HelpCommand(self, help_command))
176
177     def _setup_command(self, command):
178         """Attach some UI specific data to `command`.  After this
179         point, the :attr:`commands` are read only.
180         """
181         command._cl_name = self._safe_name(command.name)
182         command._cl_aliases = [self._safe_name(n) for n in command.aliases]
183         for argument in command.arguments:
184             argument._cl_name = self._safe_name(argument.name)
185             argument._cl_aliases = [self._safe_name(n)
186                                     for n in argument.aliases]
187
188
189 class CommandLine (UserInterface):
190     """Command line interface.  Simple and powerful.
191     """
192     def __init__(self):
193         super(CommandLine, self).__init__(name='command line')
194
195     def run(self, commands, ui_to_command_queue, command_to_ui_queue):
196         cmd = HookeCmd(commands,
197                        inqueue=ui_to_command_queue,
198                        outqueue=command_to_ui_queue)
199         cmd.cmdloop(self._splash_text())