# Copyright (C) 2009-2012 Chris Ball # W. Trevor King # # 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 . """ 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 list(options.items()): if '_' in name: # reconstruct original option name options[name.replace('_', '-')] = options.pop(name) for name,value in list(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(command_option.callback( self.command, command_option, value), file=self.command.stdout) 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('\n'.join(comps), file=self.command.stdout) 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 str(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 ...]] Options: -h, --help Print a help message. --complete Print a list of possible completions. --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 as e: print('\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', ]), file=ui.io.stdout) return 1 except libbe.command.UsageError as e: print('Usage Error:\n', e, file=ui.io.stdout) if e.command: print(e.command.usage(), file=ui.io.stdout) print('For usage information, try', file=ui.io.stdout) print(' be help %s' % e.command_name, file=ui.io.stdout) return 1 except libbe.command.UserError as e: print('ERROR:\n', e, file=ui.io.stdout) return 1 except OSError as e: print('OSError:\n', e, file=ui.io.stdout) return 1 except libbe.storage.ConnectionError as e: print('Connection Error:\n', e, file=ui.io.stdout) return 1 except libbe.util.http.HTTPError as e: print('HTTP Error:\n', e, file=ui.io.stdout) return 1 except (libbe.util.id.MultipleIDMatches, libbe.util.id.NoIDMatches, libbe.util.id.InvalidIDStructure) as e: print('Invalid id:\n', e, file=ui.io.stdout) 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 as e: if isinstance(e.command, BE): # no command given, print usage string print('Usage Error:\n', e, file=ui.io.stdout) print(be.usage(), file=ui.io.stdout) print('For example, try', file=ui.io.stdout) print(' be help', file=ui.io.stdout) else: print('Usage Error:\n', e, file=ui.io.stdout) if e.command: print(e.command.usage(), file=ui.io.stdout) print('For usage information, try', file=ui.io.stdout) print(' be help %s' % e.command_name, file=ui.io.stdout) return 1 command_name = args.pop(0) try: Class = libbe.command.get_command_class(command_name=command_name) except libbe.command.UnknownCommand as e: print(e, file=ui.io.stdout) 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 as e: print('IOError:\n', e, file=ui.io.stdout) return 1 return ret if __name__ == '__main__': sys.exit(main())