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