1 # Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org>
2 # W. Trevor King <wking@tremily.us>
4 # This file is part of Bugs Everywhere.
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
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
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/>.
20 A command line interface to Bugs Everywhere.
31 import libbe.command.help
32 import libbe.command.util
35 import libbe.ui.util.pager
36 import libbe.util.encoding
37 import libbe.util.http
40 if libbe.TESTING == True:
43 class CallbackExit (Exception):
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())
58 def _add_option(self, option):
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('-', '_'),
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
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)
83 opt = optparse.Option(long_opt, **kwargs)
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():
97 option = self._option_by_name[name]
98 if option.arg != None:
100 if value == '--complete':
102 indices = [i for i,arg in enumerate(args)
103 if arg == '--complete']
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]:
109 if name == option.name:
111 elif option.short_name != None \
112 and args[i-1].startswith('-') \
113 and args[i-1].endswith(option.short_name):
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
129 argument = self.command.args[-1]
130 if argument.repeatable == False:
131 raise libbe.command.UserError('Too many arguments')
132 if arg == '--complete':
134 if i < len(parsed_args) - 1:
135 fragment = parsed_args[i+1]
136 self.complete(argument, fragment)
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)
151 def callback(self, option, opt, value, parser):
152 command_option = option._option
153 if command_option.name == 'complete':
156 if len(parser.rargs) > 0:
157 fragment = parser.rargs[0]
158 self.complete(argument, fragment)
160 print >> self.command.stdout, command_option.callback(
161 self.command, command_option, value)
164 def complete(self, argument=None, fragment=None):
165 comps = self.command.complete(argument, fragment)
167 comps = [c for c in comps if c.startswith(fragment)]
169 print >> self.command.stdout, '\n'.join(comps)
172 def process_raw_argument(self, argument, value):
173 if value == argument.default:
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)
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.
187 >>> ui = libbe.command.UserInterface()
188 >>> ui.io.stdout = sys.stdout
190 >>> ui.io.setup_command(be)
191 >>> p = CmdOptionParser(be)
192 >>> p.exit_after_callback = False
194 ... options,args = p.parse_args(['--help']) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
195 ... except CallbackExit:
197 usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]
200 -h, --help Print a help message.
202 --complete Print a list of possible completions.
204 --version Print version string.
207 ... options,args = p.parse_args(['--complete']) # doctest: +ELLIPSIS
208 ... except CallbackExit:
209 ... print ' got callback'
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 '
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.'),
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)
255 return 'usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]'
257 def _long_help(self):
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]))
265 longest_cmd_len = max([len(name) for name,desc in cmdlist])
266 ret = ['Bugs Everywhere - Distributed bug tracking',
268 for name, desc in cmdlist:
269 numExtraSpaces = longest_cmd_len-len(name)
270 ret.append('be {}{} {}'.format(name, ' '*numExtraSpaces, desc))
272 ret.extend(['', 'Topics:'])
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))
281 ret.extend(['', 'Run', ' be help [command|topic]',
282 'for more information.'])
283 return '\n'.join(ret)
285 def version(self, *args):
286 return libbe.version.version(verbose=False)
288 def full_version(self, *args):
289 return libbe.version.version(verbose=True)
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
298 self.setup_command(be)
301 def dispatch(ui, command, args):
302 parser = CmdOptionParser(command)
304 options,args = parser.parse_args(args)
305 ret = ui.run(command, options, args)
308 except UnicodeDecodeError, e:
309 print >> ui.io.stdout, '\n'.join([
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',
316 except libbe.command.UsageError, e:
317 print >> ui.io.stdout, 'Usage Error:\n', e
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
323 except libbe.command.UserError, e:
324 print >> ui.io.stdout, 'ERROR:\n', e
327 print >> ui.io.stdout, 'OSError:\n', e
329 except libbe.storage.ConnectionError, e:
330 print >> ui.io.stdout, 'Connection Error:\n', e
332 except libbe.util.http.HTTPError, e:
333 print >> ui.io.stdout, 'HTTP Error:\n', e
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
344 locale.setlocale(locale.LC_ALL, '')
345 io = libbe.command.StdInputOutput()
350 parser = CmdOptionParser(be)
352 options,args = parser.parse_args()
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'
363 print >> ui.io.stdout, 'Usage Error:\n', e
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
370 command_name = args.pop(0)
372 Class = libbe.command.get_command_class(command_name=command_name)
373 except libbe.command.UnknownCommand, e:
374 print >> ui.io.stdout, e
377 ui.storage_callbacks = libbe.command.StorageCallbacks(options['repo'])
378 command = Class(ui=ui, server=options['server'])
379 ui.setup_command(command)
382 'new', 'comment', 'commit', 'html', 'import-xml', 'serve-storage',
387 if options['paginate'] == True:
389 if options['no-pager'] == True:
391 libbe.ui.util.pager.run_pager(paginate)
393 ret = dispatch(ui, command, args)
397 print >> ui.io.stdout, 'IOError:\n', e
402 if __name__ == '__main__':