1 # Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org>
2 # W. Trevor King <wking@drexel.edu>
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.
30 import libbe.command.help
31 import libbe.command.util
34 import libbe.ui.util.pager
35 import libbe.util.encoding
36 import libbe.util.http
39 if libbe.TESTING == True:
42 class CallbackExit (Exception):
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())
57 def _add_option(self, option):
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('-', '_'),
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
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)
82 opt = optparse.Option(long_opt, **kwargs)
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():
96 option = self._option_by_name[name]
97 if option.arg != None:
99 if value == '--complete':
101 indices = [i for i,arg in enumerate(args)
102 if arg == '--complete']
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]:
108 if name == option.name:
110 elif option.short_name != None \
111 and args[i-1].startswith('-') \
112 and args[i-1].endswith(option.short_name):
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
128 argument = self.command.args[-1]
129 if argument.repeatable == False:
130 raise libbe.command.UserError('Too many arguments')
131 if arg == '--complete':
133 if i < len(parsed_args) - 1:
134 fragment = parsed_args[i+1]
135 self.complete(argument, fragment)
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)
150 def callback(self, option, opt, value, parser):
151 command_option = option._option
152 if command_option.name == 'complete':
155 if len(parser.rargs) > 0:
156 fragment = parser.rargs[0]
157 self.complete(argument, fragment)
159 print >> self.command.stdout, command_option.callback(
160 self.command, command_option, value)
163 def complete(self, argument=None, fragment=None):
164 comps = self.command.complete(argument, fragment)
166 comps = [c for c in comps if c.startswith(fragment)]
168 print >> self.command.stdout, '\n'.join(comps)
171 def process_raw_argument(self, argument, value):
172 if value == argument.default:
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)
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.
186 >>> ui = libbe.command.UserInterface()
187 >>> ui.io.stdout = sys.stdout
189 >>> ui.io.setup_command(be)
190 >>> p = CmdOptionParser(be)
191 >>> p.exit_after_callback = False
193 ... options,args = p.parse_args(['--help']) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
194 ... except CallbackExit:
196 usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]
199 -h, --help Print a help message.
201 --complete Print a list of possible completions.
203 --version Print version string.
206 ... options,args = p.parse_args(['--complete']) # doctest: +ELLIPSIS
207 ... except CallbackExit:
208 ... print ' got callback'
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 '
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.'),
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)
254 return 'usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]'
256 def _long_help(self):
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]))
264 longest_cmd_len = max([len(name) for name,desc in cmdlist])
265 ret = ['Bugs Everywhere - Distributed bug tracking',
267 for name, desc in cmdlist:
268 numExtraSpaces = longest_cmd_len-len(name)
269 ret.append('be {}{} {}'.format(name, ' '*numExtraSpaces, desc))
271 ret.extend(['', 'Topics:'])
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))
280 ret.extend(['', 'Run', ' be help [command|topic]',
281 'for more information.'])
282 return '\n'.join(ret)
284 def version(self, *args):
285 return libbe.version.version(verbose=False)
287 def full_version(self, *args):
288 return libbe.version.version(verbose=True)
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
297 self.setup_command(be)
300 def dispatch(ui, command, args):
301 parser = CmdOptionParser(command)
303 options,args = parser.parse_args(args)
304 ret = ui.run(command, options, args)
307 except UnicodeDecodeError, e:
308 print >> ui.io.stdout, '\n'.join([
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',
315 except libbe.command.UsageError, e:
316 print >> ui.io.stdout, 'Usage Error:\n', e
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
322 except libbe.command.UserError, e:
323 print >> ui.io.stdout, 'ERROR:\n', e
326 print >> ui.io.stdout, 'OSError:\n', e
328 except libbe.storage.ConnectionError, e:
329 print >> ui.io.stdout, 'Connection Error:\n', e
331 except libbe.util.http.HTTPError, e:
332 print >> ui.io.stdout, 'HTTP Error:\n', e
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
343 io = libbe.command.StdInputOutput()
348 parser = CmdOptionParser(be)
350 options,args = parser.parse_args()
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'
361 print >> ui.io.stdout, 'Usage Error:\n', e
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
368 command_name = args.pop(0)
370 Class = libbe.command.get_command_class(command_name=command_name)
371 except libbe.command.UnknownCommand, e:
372 print >> ui.io.stdout, e
375 ui.storage_callbacks = libbe.command.StorageCallbacks(options['repo'])
376 command = Class(ui=ui, server=options['server'])
377 ui.setup_command(command)
380 'new', 'comment', 'commit', 'html', 'import-xml', 'serve-storage',
385 if options['paginate'] == True:
387 if options['no-pager'] == True:
389 libbe.ui.util.pager.run_pager(paginate)
391 ret = dispatch(ui, command, args)
395 print >> ui.io.stdout, 'IOError:\n', e
400 if __name__ == '__main__':