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