# Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org>
# W. Trevor King <wking@tremily.us>
#
# This file is part of Bugs Everywhere.
#
# Bugs Everywhere is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 2 of the License, or (at your option) any
# later version.
#
# Bugs Everywhere is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
"""
A command line interface to Bugs Everywhere.
"""
import optparse
import os
import sys
import locale
import libbe
import libbe.bugdir
import libbe.command
import libbe.command.help
import libbe.command.util
import libbe.storage
import libbe.version
import libbe.ui.util.pager
import libbe.util.encoding
import libbe.util.http
if libbe.TESTING == True:
import doctest
class CallbackExit (Exception):
pass
class CmdOptionParser(optparse.OptionParser):
def __init__(self, command):
self.command = command
optparse.OptionParser.__init__(self)
self.remove_option('-h')
self.disable_interspersed_args()
self._option_by_name = {}
for option in self.command.options:
self._add_option(option)
self.set_usage(command.usage())
def _add_option(self, option):
option.validate()
self._option_by_name[option.name] = option
long_opt = '--%s' % option.name
if option.short_name != None:
short_opt = '-%s' % option.short_name
assert '_' not in option.name, \
'Non-reconstructable option name %s' % option.name
kwargs = {'dest':option.name.replace('-', '_'),
'help':option.help}
if option.arg == None: # a callback option
kwargs['action'] = 'callback'
kwargs['callback'] = self.callback
elif option.arg.type == 'bool':
kwargs['action'] = 'store_true'
kwargs['metavar'] = None
kwargs['default'] = False
else:
kwargs['type'] = option.arg.type
kwargs['action'] = 'store'
kwargs['metavar'] = option.arg.metavar
kwargs['default'] = option.arg.default
if option.short_name != None:
opt = optparse.Option(short_opt, long_opt, **kwargs)
else:
opt = optparse.Option(long_opt, **kwargs)
opt._option = option
self.add_option(opt)
def parse_args(self, args=None, values=None):
args = self._get_args(args)
options,parsed_args = optparse.OptionParser.parse_args(
self, args=args, values=values)
options = options.__dict__
for name,value in options.items():
if '_' in name: # reconstruct original option name
options[name.replace('_', '-')] = options.pop(name)
for name,value in options.items():
argument = None
option = self._option_by_name[name]
if option.arg != None:
argument = option.arg
if value == '--complete':
fragment = None
indices = [i for i,arg in enumerate(args)
if arg == '--complete']
for i in indices:
assert i > 0 # this --complete is an option value
if args[i-1] in ['--%s' % o.name
for o in self.command.options]:
name = args[i-1][2:]
if name == option.name:
break
elif option.short_name != None \
and args[i-1].startswith('-') \
and args[i-1].endswith(option.short_name):
break
if i+1 < len(args):
fragment = args[i+1]
self.complete(argument, fragment)
elif argument is not None:
value = self.process_raw_argument(argument=argument, value=value)
options[name] = value
for i,arg in enumerate(parsed_args):
if i > 0 and self.command.name == 'be':
break # let this pass through for the command parser to handle
elif i < len(self.command.args):
argument = self.command.args[i]
elif len(self.command.args) == 0:
break # command doesn't take arguments
else:
argument = self.command.args[-1]
if argument.repeatable == False:
raise libbe.command.UserError('Too many arguments')
if arg == '--complete':
fragment = None
if i < len(parsed_args) - 1:
fragment = parsed_args[i+1]
self.complete(argument, fragment)
else:
value = self.process_raw_argument(argument=argument, value=arg)
parsed_args[i] = value
if (len(parsed_args) > len(self.command.args) and
(len(self.command.args) == 0 or
self.command.args[-1].repeatable == False)):
raise libbe.command.UserError('Too many arguments')
for arg in self.command.args[len(parsed_args):]:
if arg.optional == False:
raise libbe.command.UsageError(
command=self.command,
message='Missing required argument %s' % arg.metavar)
return (options, parsed_args)
def callback(self, option, opt, value, parser):
command_option = option._option
if command_option.name == 'complete':
argument = None
fragment = None
if len(parser.rargs) > 0:
fragment = parser.rargs[0]
self.complete(argument, fragment)
else:
print >> self.command.stdout, command_option.callback(
self.command, command_option, value)
raise CallbackExit
def complete(self, argument=None, fragment=None):
comps = self.command.complete(argument, fragment)
if fragment != None:
comps = [c for c in comps if c.startswith(fragment)]
if len(comps) > 0:
print >> self.command.stdout, '\n'.join(comps)
raise CallbackExit
def process_raw_argument(self, argument, value):
if value == argument.default:
return value
if argument.type == 'string':
if not hasattr(self, 'argv_encoding'):
self.argv_encoding = libbe.util.encoding.get_argv_encoding()
return unicode(value, self.argv_encoding)
return value
class BE (libbe.command.Command):
"""Class for parsing the command line arguments for `be`.
This class does not contain a useful _run() method. Call this
module's main() function instead.
>>> ui = libbe.command.UserInterface()
>>> ui.io.stdout = sys.stdout
>>> be = BE(ui=ui)
>>> ui.io.setup_command(be)
>>> p = CmdOptionParser(be)
>>> p.exit_after_callback = False
>>> try:
... options,args = p.parse_args(['--help']) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
... except CallbackExit:
... pass
usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]
<BLANKLINE>
Options:
-h, --help Print a help message.
<BLANKLINE>
--complete Print a list of possible completions.
<BLANKLINE>
--version Print version string.
...
>>> try:
... options,args = p.parse_args(['--complete']) # doctest: +ELLIPSIS
... except CallbackExit:
... print ' got callback'
--help
--version
...
subscribe
tag
target
got callback
"""
name = 'be'
def __init__(self, *args, **kwargs):
libbe.command.Command.__init__(self, *args, **kwargs)
self.options.extend([
libbe.command.Option(name='version',
help='Print version string.',
callback=self.version),
libbe.command.Option(name='full-version',
help='Print full version information.',
callback=self.full_version),
libbe.command.Option(name='repo', short_name='r',
help='Select BE repository (see `be help repo`) rather '
'than the current directory.',
arg=libbe.command.Argument(
name='repo', metavar='REPO', default='.',
completion_callback=libbe.command.util.complete_path)),
libbe.command.Option(name='server', short_name='s',
help='Select BE command server (see `be help '
'server`) rather than executing commands '
'locally',
arg=libbe.command.Argument(
name='server', metavar='URL')),
libbe.command.Option(name='paginate',
help='Pipe all output into less (or if set, $PAGER).'),
libbe.command.Option(name='no-pager',
help='Do not pipe git output into a pager.'),
])
self.args.extend([
libbe.command.Argument(
name='command', optional=False,
completion_callback=libbe.command.util.complete_command),
libbe.command.Argument(
name='args', optional=True, repeatable=True)
])
def usage(self):
return 'usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]'
def _long_help(self):
cmdlist = []
for name in libbe.command.commands():
Class = libbe.command.get_command_class(command_name=name)
assert hasattr(Class, '__doc__') and Class.__doc__ != None, \
'Command class %s missing docstring' % Class
cmdlist.append((Class.name, Class.__doc__.splitlines()[0]))
cmdlist.sort()
longest_cmd_len = max([len(name) for name,desc in cmdlist])
ret = ['Bugs Everywhere - Distributed bug tracking',
'', 'Commands:']
for name, desc in cmdlist:
numExtraSpaces = longest_cmd_len-len(name)
ret.append('be {}{} {}'.format(name, ' '*numExtraSpaces, desc))
ret.extend(['', 'Topics:'])
topic_list = [
(name,desc.splitlines()[0])
for name,desc in sorted(libbe.command.help.TOPICS.items())]
longest_topic_len = max([len(name) for name,desc in topic_list])
for name,desc in topic_list:
extra_spaces = longest_topic_len - len(name)
ret.append('{}{} {}'.format(name, ' '*extra_spaces, desc))
ret.extend(['', 'Run', ' be help [command|topic]',
'for more information.'])
return '\n'.join(ret)
def version(self, *args):
return libbe.version.version(verbose=False)
def full_version(self, *args):
return libbe.version.version(verbose=True)
class CommandLine (libbe.command.UserInterface):
def __init__(self, *args, **kwargs):
libbe.command.UserInterface.__init__(self, *args, **kwargs)
self.restrict_file_access = False
self.storage_callbacks = None
def help(self):
be = BE(ui=self)
self.setup_command(be)
return be.help()
def dispatch(ui, command, args):
parser = CmdOptionParser(command)
try:
options,args = parser.parse_args(args)
ret = ui.run(command, options, args)
except CallbackExit:
return 0
except UnicodeDecodeError, e:
print >> ui.io.stdout, '\n'.join([
'ERROR:', str(e),
'You should set a locale that supports unicode, e.g.',
' export LANG=en_US.utf8',
'See http://docs.python.org/library/locale.html for details',
])
return 1
except libbe.command.UsageError, e:
print >> ui.io.stdout, 'Usage Error:\n', e
if e.command:
print >> ui.io.stdout, e.command.usage()
print >> ui.io.stdout, 'For usage information, try'
print >> ui.io.stdout, ' be help %s' % e.command_name
return 1
except libbe.command.UserError, e:
print >> ui.io.stdout, 'ERROR:\n', e
return 1
except OSError, e:
print >> ui.io.stdout, 'OSError:\n', e
return 1
except libbe.storage.ConnectionError, e:
print >> ui.io.stdout, 'Connection Error:\n', e
return 1
except libbe.util.http.HTTPError, e:
print >> ui.io.stdout, 'HTTP Error:\n', e
return 1
except (libbe.util.id.MultipleIDMatches, libbe.util.id.NoIDMatches,
libbe.util.id.InvalidIDStructure), e:
print >> ui.io.stdout, 'Invalid id:\n', e
return 1
finally:
command.cleanup()
return ret
def main():
locale.setlocale(locale.LC_ALL, '')
io = libbe.command.StdInputOutput()
ui = CommandLine(io)
be = BE(ui=ui)
ui.setup_command(be)
parser = CmdOptionParser(be)
try:
options,args = parser.parse_args()
except CallbackExit:
return 0
except libbe.command.UsageError, e:
if isinstance(e.command, BE):
# no command given, print usage string
print >> ui.io.stdout, 'Usage Error:\n', e
print >> ui.io.stdout, be.usage()
print >> ui.io.stdout, 'For example, try'
print >> ui.io.stdout, ' be help'
else:
print >> ui.io.stdout, 'Usage Error:\n', e
if e.command:
print >> ui.io.stdout, e.command.usage()
print >> ui.io.stdout, 'For usage information, try'
print >> ui.io.stdout, ' be help %s' % e.command_name
return 1
command_name = args.pop(0)
try:
Class = libbe.command.get_command_class(command_name=command_name)
except libbe.command.UnknownCommand, e:
print >> ui.io.stdout, e
return 1
ui.storage_callbacks = libbe.command.StorageCallbacks(options['repo'])
command = Class(ui=ui, server=options['server'])
ui.setup_command(command)
if command.name in [
'new', 'comment', 'commit', 'html', 'import-xml', 'serve-commands']:
paginate = 'never'
else:
paginate = 'auto'
if options['paginate'] == True:
paginate = 'always'
if options['no-pager'] == True:
paginate = 'never'
libbe.ui.util.pager.run_pager(paginate)
ret = dispatch(ui, command, args)
try:
ui.cleanup()
except IOError, e:
print >> ui.io.stdout, 'IOError:\n', e
return 1
return ret
if __name__ == '__main__':
sys.exit(main())