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