Merge assorted bugfixes and optimizations.
[be.git] / libbe / ui / command_line.py
1 # Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
2 #                         Chris Ball <cjb@laptop.org>
3 #                         Gianluca Montecchi <gian@grys.it>
4 #                         Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
5 #                         W. Trevor King <wking@drexel.edu>
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along
18 # with this program; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21 """
22 A command line interface to Bugs Everywhere.
23 """
24
25 import optparse
26 import os
27 import sys
28
29 import libbe
30 import libbe.bugdir
31 import libbe.command
32 import libbe.command.util
33 import libbe.version
34 import libbe.ui.util.pager
35
36 if libbe.TESTING == True:
37     import doctest
38
39 class CallbackExit (Exception):
40     pass
41
42 class CmdOptionParser(optparse.OptionParser):
43     def __init__(self, command):
44         self.command = command
45         optparse.OptionParser.__init__(self)
46         self.remove_option('-h')
47         self.disable_interspersed_args()
48         self._option_by_name = {}
49         for option in self.command.options:
50             self._add_option(option)
51         self.set_usage(command.usage())
52
53
54     def _add_option(self, option):
55         option.validate()
56         self._option_by_name[option.name] = option
57         long_opt = '--%s' % option.name
58         if option.short_name != None:
59             short_opt = '-%s' % option.short_name
60         assert '_' not in option.name, \
61             'Non-reconstructable option name %s' % option.name
62         kwargs = {'dest':option.name.replace('-', '_'),
63                   'help':option.help}
64         if option.arg == None: # a callback option
65             kwargs['action'] = 'callback'
66             kwargs['callback'] = self.callback
67         elif option.arg.type == 'bool':
68             kwargs['action'] = 'store_true'
69             kwargs['metavar'] = None
70             kwargs['default'] = False
71         else:
72             kwargs['type'] = option.arg.type
73             kwargs['action'] = 'store'
74             kwargs['metavar'] = option.arg.metavar
75             kwargs['default'] = option.arg.default
76         if option.short_name != None:
77             opt = optparse.Option(short_opt, long_opt, **kwargs)
78         else:
79             opt = optparse.Option(long_opt, **kwargs)
80         opt._option = option
81         self.add_option(opt)
82
83     def parse_args(self, args=None, values=None):
84         args = self._get_args(args)
85         options,parsed_args = optparse.OptionParser.parse_args(
86             self, args=args, values=values)
87         options = options.__dict__
88         for name,value in options.items():
89             if '_' in name: # reconstruct original option name
90                 options[name.replace('_', '-')] = options.pop(name)
91         for name,value in options.items():
92             if value == '--complete':
93                 argument = None
94                 option = self._option_by_name[name]
95                 if option.arg != None:
96                     argument = option.arg
97                 fragment = None
98                 indices = [i for i,arg in enumerate(args)
99                            if arg == '--complete']
100                 for i in indices:
101                     assert i > 0  # this --complete is an option value
102                     if args[i-1] in ['--%s' % o.name
103                                      for o in self.command.options]:
104                         name = args[i-1][2:]
105                         if name == option.name:
106                             break
107                     elif option.short_name != None \
108                             and args[i-1].startswith('-') \
109                             and args[i-1].endswith(option.short_name):
110                         break
111                 if i+1 < len(args):
112                     fragment = args[i+1]
113                 self.complete(argument, fragment)
114         for i,arg in enumerate(parsed_args):
115             if arg == '--complete':
116                 if i > 0 and self.command.name == 'be':
117                     break # let this pass through for the command parser to handle
118                 elif i < len(self.command.args):
119                     argument = self.command.args[i]
120                 elif len(self.command.args) == 0:
121                     break # command doesn't take arguments
122                 else:
123                     argument = self.command.args[-1]
124                     if argument.repeatable == False:
125                         raise libbe.command.UserError('Too many arguments')
126                 fragment = None
127                 if i < len(parsed_args) - 1:
128                     fragment = parsed_args[i+1]
129                 self.complete(argument, fragment)
130         if len(parsed_args) > len(self.command.args) \
131                 and self.command.args[-1].repeatable == False:
132             raise libbe.command.UserError('Too many arguments')
133         for arg in self.command.args[len(parsed_args):]:
134             if arg.optional == False:
135                 raise libbe.command.UserError(
136                     'Missing required argument %s' % arg.metavar)
137         return (options, parsed_args)
138
139     def callback(self, option, opt, value, parser):
140         command_option = option._option
141         if command_option.name == 'complete':
142             argument = None
143             fragment = None
144             if len(parser.rargs) > 0:
145                 fragment = parser.rargs[0]
146             self.complete(argument, fragment)
147         else:
148             print >> self.command.stdout, command_option.callback(
149                 self.command, command_option, value)
150         raise CallbackExit
151
152     def complete(self, argument=None, fragment=None):
153         comps = self.command.complete(argument, fragment)
154         if fragment != None:
155             comps = [c for c in comps if c.startswith(fragment)]
156         if len(comps) > 0:
157             print >> self.command.stdout, '\n'.join(comps)
158         raise CallbackExit
159
160 class BE (libbe.command.Command):
161     """Class for parsing the command line arguments for `be`.
162     This class does not contain a useful _run() method.  Call this
163     module's main() function instead.
164
165     >>> ui = libbe.command.UserInterface()
166     >>> ui.io.stdout = sys.stdout
167     >>> be = BE(ui=ui)
168     >>> ui.io.setup_command(be)
169     >>> p = CmdOptionParser(be)
170     >>> p.exit_after_callback = False
171     >>> try:
172     ...     options,args = p.parse_args(['--help']) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
173     ... except CallbackExit:
174     ...     pass
175     usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]
176     <BLANKLINE>
177     Options:
178       -h, --help         Print a help message.
179     <BLANKLINE>
180       --complete         Print a list of possible completions.
181     <BLANKLINE>
182       --version          Print version string.
183     ...
184     >>> try:
185     ...     options,args = p.parse_args(['--complete']) # doctest: +ELLIPSIS
186     ... except CallbackExit:
187     ...     print '  got callback'
188     --help
189     --complete
190     --version
191     ...
192     subscribe
193     tag
194     target
195       got callback
196     """
197     name = 'be'
198
199     def __init__(self, *args, **kwargs):
200         libbe.command.Command.__init__(self, *args, **kwargs)
201         self.options.extend([
202                 libbe.command.Option(name='version',
203                     help='Print version string.',
204                     callback=self.version),
205                 libbe.command.Option(name='full-version',
206                     help='Print full version information.',
207                     callback=self.full_version),
208                 libbe.command.Option(name='repo', short_name='r',
209                     help='Select BE repository (see `be help repo`) rather '
210                          'than the current directory.',
211                     arg=libbe.command.Argument(
212                         name='repo', metavar='REPO', default='.',
213                         completion_callback=libbe.command.util.complete_path)),
214                 libbe.command.Option(name='paginate',
215                     help='Pipe all output into less (or if set, $PAGER).'),
216                 libbe.command.Option(name='no-pager',
217                     help='Do not pipe git output into a pager.'),
218                 ])
219         self.args.extend([
220                 libbe.command.Argument(
221                     name='command', optional=False,
222                     completion_callback=libbe.command.util.complete_command),
223                 libbe.command.Argument(
224                     name='args', optional=True, repeatable=True)
225                 ])
226
227     def usage(self):
228         return 'usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]'
229
230     def _long_help(self):
231         cmdlist = []
232         for name in libbe.command.commands():
233             Class = libbe.command.get_command_class(command_name=name)
234             assert hasattr(Class, '__doc__') and Class.__doc__ != None, \
235                 'Command class %s missing docstring' % Class
236             cmdlist.append((name, Class.__doc__.splitlines()[0]))
237         cmdlist.sort()
238         longest_cmd_len = max([len(name) for name,desc in cmdlist])
239         ret = ['Bugs Everywhere - Distributed bug tracking',
240                '', 'Supported commands']
241         for name, desc in cmdlist:
242             numExtraSpaces = longest_cmd_len-len(name)
243             ret.append('be %s%*s    %s' % (name, numExtraSpaces, '', desc))
244         ret.extend(['', 'Run', '  be help [command]', 'for more information.'])
245         return '\n'.join(ret)
246
247     def version(self, *args):
248         return libbe.version.version(verbose=False)
249
250     def full_version(self, *args):
251         return libbe.version.version(verbose=True)
252
253 class CommandLine (libbe.command.UserInterface):
254     def __init__(self, *args, **kwargs):
255         libbe.command.UserInterface.__init__(self, *args, **kwargs)
256         self.restrict_file_access = False
257         self.storage_callbacks = None
258     def help(self):
259         be = BE(ui=self)
260         self.setup_command(be)
261         return be.help()
262
263 def dispatch(ui, command, args):
264     parser = CmdOptionParser(command)
265     try:
266         options,args = parser.parse_args(args)
267         ret = ui.run(command, options, args)
268     except CallbackExit:
269         return 0
270     except libbe.command.UserError, e:
271         print >> ui.io.stdout, 'ERROR:\n', e
272         return 1
273     except libbe.storage.ConnectionError, e:
274         print >> ui.io.stdout, 'Connection Error:\n', e
275         return 1
276     except (libbe.util.id.MultipleIDMatches, libbe.util.id.NoIDMatches,
277             libbe.util.id.InvalidIDStructure), e:
278         print >> ui.io.stdout, 'Invalid id:\n', e
279         return 1
280     finally:
281         command.cleanup()
282     return ret
283
284 def main():
285     io = libbe.command.StdInputOutput()
286     ui = CommandLine(io)
287     be = BE(ui=ui)
288     ui.setup_command(be)
289
290     parser = CmdOptionParser(be)
291     try:
292         options,args = parser.parse_args()
293     except CallbackExit:
294         return 0
295     except libbe.command.UserError, e:
296         if str(e).endswith('COMMAND'):
297             # no command given, print usage string
298             print >> ui.io.stdout, 'ERROR:'
299             print >> ui.io.stdout, be.usage(), '\n', e
300             print >> ui.io.stdout, 'For example, try'
301             print >> ui.io.stdout, '  be help'
302         else:
303             print >> ui.io.stdout, 'ERROR:\n', e
304         return 1
305
306     command_name = args.pop(0)
307     try:
308         Class = libbe.command.get_command_class(command_name=command_name)
309     except libbe.command.UnknownCommand, e:
310         print >> ui.io.stdout, e
311         return 1
312
313     ui.storage_callbacks = libbe.command.StorageCallbacks(options['repo'])
314     command = Class(ui=ui)
315     ui.setup_command(command)
316
317     if command.name in ['comment', 'commit', 'import-xml', 'serve']:
318         paginate = 'never'
319     else:
320         paginate = 'auto'
321     if options['paginate'] == True:
322         paginate = 'always'
323     if options['no-pager'] == True:
324         paginate = 'never'
325     libbe.ui.util.pager.run_pager(paginate)
326
327     ret = dispatch(ui, command, args)
328     ui.cleanup()
329     return ret
330
331 if __name__ == '__main__':
332     sys.exit(main())