WebKitTools/Scripts/webkitpy/tool/multicommandtool.py
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     1 # Copyright (c) 2009 Google Inc. All rights reserved.
       
     2 # Copyright (c) 2009 Apple Inc. All rights reserved.
       
     3 #
       
     4 # Redistribution and use in source and binary forms, with or without
       
     5 # modification, are permitted provided that the following conditions are
       
     6 # met:
       
     7 # 
       
     8 #     * Redistributions of source code must retain the above copyright
       
     9 # notice, this list of conditions and the following disclaimer.
       
    10 #     * Redistributions in binary form must reproduce the above
       
    11 # copyright notice, this list of conditions and the following disclaimer
       
    12 # in the documentation and/or other materials provided with the
       
    13 # distribution.
       
    14 #     * Neither the name of Google Inc. nor the names of its
       
    15 # contributors may be used to endorse or promote products derived from
       
    16 # this software without specific prior written permission.
       
    17 # 
       
    18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
       
    19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
       
    20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
       
    21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
       
    22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
       
    23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
       
    24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
       
    25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
       
    26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
       
    27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
       
    28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    29 #
       
    30 # MultiCommandTool provides a framework for writing svn-like/git-like tools
       
    31 # which are called with the following format:
       
    32 # tool-name [global options] command-name [command options]
       
    33 
       
    34 import sys
       
    35 
       
    36 from optparse import OptionParser, IndentedHelpFormatter, SUPPRESS_USAGE, make_option
       
    37 
       
    38 from webkitpy.tool.grammar import pluralize
       
    39 from webkitpy.common.system.deprecated_logging import log
       
    40 
       
    41 
       
    42 class TryAgain(Exception):
       
    43     pass
       
    44 
       
    45 
       
    46 class Command(object):
       
    47     name = None
       
    48     show_in_main_help = False
       
    49     def __init__(self, help_text, argument_names=None, options=None, long_help=None, requires_local_commits=False):
       
    50         self.help_text = help_text
       
    51         self.long_help = long_help
       
    52         self.argument_names = argument_names
       
    53         self.required_arguments = self._parse_required_arguments(argument_names)
       
    54         self.options = options
       
    55         self.requires_local_commits = requires_local_commits
       
    56         self.tool = None
       
    57         # option_parser can be overriden by the tool using set_option_parser
       
    58         # This default parser will be used for standalone_help printing.
       
    59         self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options)
       
    60 
       
    61     # This design is slightly awkward, but we need the
       
    62     # the tool to be able to create and modify the option_parser
       
    63     # before it knows what Command to run.
       
    64     def set_option_parser(self, option_parser):
       
    65         self.option_parser = option_parser
       
    66         self._add_options_to_parser()
       
    67 
       
    68     def _add_options_to_parser(self):
       
    69         options = self.options or []
       
    70         for option in options:
       
    71             self.option_parser.add_option(option)
       
    72 
       
    73     # The tool calls bind_to_tool on each Command after adding it to its list.
       
    74     def bind_to_tool(self, tool):
       
    75         # Command instances can only be bound to one tool at a time.
       
    76         if self.tool and tool != self.tool:
       
    77             raise Exception("Command already bound to tool!")
       
    78         self.tool = tool
       
    79 
       
    80     @staticmethod
       
    81     def _parse_required_arguments(argument_names):
       
    82         required_args = []
       
    83         if not argument_names:
       
    84             return required_args
       
    85         split_args = argument_names.split(" ")
       
    86         for argument in split_args:
       
    87             if argument[0] == '[':
       
    88                 # For now our parser is rather dumb.  Do some minimal validation that
       
    89                 # we haven't confused it.
       
    90                 if argument[-1] != ']':
       
    91                     raise Exception("Failure to parse argument string %s.  Argument %s is missing ending ]" % (argument_names, argument))
       
    92             else:
       
    93                 required_args.append(argument)
       
    94         return required_args
       
    95 
       
    96     def name_with_arguments(self):
       
    97         usage_string = self.name
       
    98         if self.options:
       
    99             usage_string += " [options]"
       
   100         if self.argument_names:
       
   101             usage_string += " " + self.argument_names
       
   102         return usage_string
       
   103 
       
   104     def parse_args(self, args):
       
   105         return self.option_parser.parse_args(args)
       
   106 
       
   107     def check_arguments_and_execute(self, options, args, tool=None):
       
   108         if len(args) < len(self.required_arguments):
       
   109             log("%s required, %s provided.  Provided: %s  Required: %s\nSee '%s help %s' for usage." % (
       
   110                 pluralize("argument", len(self.required_arguments)),
       
   111                 pluralize("argument", len(args)),
       
   112                 "'%s'" % " ".join(args),
       
   113                 " ".join(self.required_arguments),
       
   114                 tool.name(),
       
   115                 self.name))
       
   116             return 1
       
   117         return self.execute(options, args, tool) or 0
       
   118 
       
   119     def standalone_help(self):
       
   120         help_text = self.name_with_arguments().ljust(len(self.name_with_arguments()) + 3) + self.help_text + "\n\n"
       
   121         if self.long_help:
       
   122             help_text += "%s\n\n" % self.long_help
       
   123         help_text += self.option_parser.format_option_help(IndentedHelpFormatter())
       
   124         return help_text
       
   125 
       
   126     def execute(self, options, args, tool):
       
   127         raise NotImplementedError, "subclasses must implement"
       
   128 
       
   129     # main() exists so that Commands can be turned into stand-alone scripts.
       
   130     # Other parts of the code will likely require modification to work stand-alone.
       
   131     def main(self, args=sys.argv):
       
   132         (options, args) = self.parse_args(args)
       
   133         # Some commands might require a dummy tool
       
   134         return self.check_arguments_and_execute(options, args)
       
   135 
       
   136 
       
   137 # FIXME: This should just be rolled into Command.  help_text and argument_names do not need to be instance variables.
       
   138 class AbstractDeclarativeCommand(Command):
       
   139     help_text = None
       
   140     argument_names = None
       
   141     long_help = None
       
   142     def __init__(self, options=None, **kwargs):
       
   143         Command.__init__(self, self.help_text, self.argument_names, options=options, long_help=self.long_help, **kwargs)
       
   144 
       
   145 
       
   146 class HelpPrintingOptionParser(OptionParser):
       
   147     def __init__(self, epilog_method=None, *args, **kwargs):
       
   148         self.epilog_method = epilog_method
       
   149         OptionParser.__init__(self, *args, **kwargs)
       
   150 
       
   151     def error(self, msg):
       
   152         self.print_usage(sys.stderr)
       
   153         error_message = "%s: error: %s\n" % (self.get_prog_name(), msg)
       
   154         # This method is overriden to add this one line to the output:
       
   155         error_message += "\nType \"%s --help\" to see usage.\n" % self.get_prog_name()
       
   156         self.exit(1, error_message)
       
   157 
       
   158     # We override format_epilog to avoid the default formatting which would paragraph-wrap the epilog
       
   159     # and also to allow us to compute the epilog lazily instead of in the constructor (allowing it to be context sensitive).
       
   160     def format_epilog(self, epilog):
       
   161         if self.epilog_method:
       
   162             return "\n%s\n" % self.epilog_method()
       
   163         return ""
       
   164 
       
   165 
       
   166 class HelpCommand(AbstractDeclarativeCommand):
       
   167     name = "help"
       
   168     help_text = "Display information about this program or its subcommands"
       
   169     argument_names = "[COMMAND]"
       
   170 
       
   171     def __init__(self):
       
   172         options = [
       
   173             make_option("-a", "--all-commands", action="store_true", dest="show_all_commands", help="Print all available commands"),
       
   174         ]
       
   175         AbstractDeclarativeCommand.__init__(self, options)
       
   176         self.show_all_commands = False # A hack used to pass --all-commands to _help_epilog even though it's called by the OptionParser.
       
   177 
       
   178     def _help_epilog(self):
       
   179         # Only show commands which are relevant to this checkout's SCM system.  Might this be confusing to some users?
       
   180         if self.show_all_commands:
       
   181             epilog = "All %prog commands:\n"
       
   182             relevant_commands = self.tool.commands[:]
       
   183         else:
       
   184             epilog = "Common %prog commands:\n"
       
   185             relevant_commands = filter(self.tool.should_show_in_main_help, self.tool.commands)
       
   186         longest_name_length = max(map(lambda command: len(command.name), relevant_commands))
       
   187         relevant_commands.sort(lambda a, b: cmp(a.name, b.name))
       
   188         command_help_texts = map(lambda command: "   %s   %s\n" % (command.name.ljust(longest_name_length), command.help_text), relevant_commands)
       
   189         epilog += "%s\n" % "".join(command_help_texts)
       
   190         epilog += "See '%prog help --all-commands' to list all commands.\n"
       
   191         epilog += "See '%prog help COMMAND' for more information on a specific command.\n"
       
   192         return epilog.replace("%prog", self.tool.name()) # Use of %prog here mimics OptionParser.expand_prog_name().
       
   193 
       
   194     # FIXME: This is a hack so that we don't show --all-commands as a global option:
       
   195     def _remove_help_options(self):
       
   196         for option in self.options:
       
   197             self.option_parser.remove_option(option.get_opt_string())
       
   198 
       
   199     def execute(self, options, args, tool):
       
   200         if args:
       
   201             command = self.tool.command_by_name(args[0])
       
   202             if command:
       
   203                 print command.standalone_help()
       
   204                 return 0
       
   205 
       
   206         self.show_all_commands = options.show_all_commands
       
   207         self._remove_help_options()
       
   208         self.option_parser.print_help()
       
   209         return 0
       
   210 
       
   211 
       
   212 class MultiCommandTool(object):
       
   213     global_options = None
       
   214 
       
   215     def __init__(self, name=None, commands=None):
       
   216         self._name = name or OptionParser(prog=name).get_prog_name() # OptionParser has nice logic for fetching the name.
       
   217         # Allow the unit tests to disable command auto-discovery.
       
   218         self.commands = commands or [cls() for cls in self._find_all_commands() if cls.name]
       
   219         self.help_command = self.command_by_name(HelpCommand.name)
       
   220         # Require a help command, even if the manual test list doesn't include one.
       
   221         if not self.help_command:
       
   222             self.help_command = HelpCommand()
       
   223             self.commands.append(self.help_command)
       
   224         for command in self.commands:
       
   225             command.bind_to_tool(self)
       
   226 
       
   227     @classmethod
       
   228     def _add_all_subclasses(cls, class_to_crawl, seen_classes):
       
   229         for subclass in class_to_crawl.__subclasses__():
       
   230             if subclass not in seen_classes:
       
   231                 seen_classes.add(subclass)
       
   232                 cls._add_all_subclasses(subclass, seen_classes)
       
   233 
       
   234     @classmethod
       
   235     def _find_all_commands(cls):
       
   236         commands = set()
       
   237         cls._add_all_subclasses(Command, commands)
       
   238         return sorted(commands)
       
   239 
       
   240     def name(self):
       
   241         return self._name
       
   242 
       
   243     def _create_option_parser(self):
       
   244         usage = "Usage: %prog [options] COMMAND [ARGS]"
       
   245         return HelpPrintingOptionParser(epilog_method=self.help_command._help_epilog, prog=self.name(), usage=usage)
       
   246 
       
   247     @staticmethod
       
   248     def _split_command_name_from_args(args):
       
   249         # Assume the first argument which doesn't start with "-" is the command name.
       
   250         command_index = 0
       
   251         for arg in args:
       
   252             if arg[0] != "-":
       
   253                 break
       
   254             command_index += 1
       
   255         else:
       
   256             return (None, args[:])
       
   257 
       
   258         command = args[command_index]
       
   259         return (command, args[:command_index] + args[command_index + 1:])
       
   260 
       
   261     def command_by_name(self, command_name):
       
   262         for command in self.commands:
       
   263             if command_name == command.name:
       
   264                 return command
       
   265         return None
       
   266 
       
   267     def path(self):
       
   268         raise NotImplementedError, "subclasses must implement"
       
   269 
       
   270     def command_completed(self):
       
   271         pass
       
   272 
       
   273     def should_show_in_main_help(self, command):
       
   274         return command.show_in_main_help
       
   275 
       
   276     def should_execute_command(self, command):
       
   277         return True
       
   278 
       
   279     def _add_global_options(self, option_parser):
       
   280         global_options = self.global_options or []
       
   281         for option in global_options:
       
   282             option_parser.add_option(option)
       
   283 
       
   284     def handle_global_options(self, options):
       
   285         pass
       
   286 
       
   287     def main(self, argv=sys.argv):
       
   288         (command_name, args) = self._split_command_name_from_args(argv[1:])
       
   289 
       
   290         option_parser = self._create_option_parser()
       
   291         self._add_global_options(option_parser)
       
   292 
       
   293         command = self.command_by_name(command_name) or self.help_command
       
   294         if not command:
       
   295             option_parser.error("%s is not a recognized command" % command_name)
       
   296 
       
   297         command.set_option_parser(option_parser)
       
   298         (options, args) = command.parse_args(args)
       
   299         self.handle_global_options(options)
       
   300 
       
   301         (should_execute, failure_reason) = self.should_execute_command(command)
       
   302         if not should_execute:
       
   303             log(failure_reason)
       
   304             return 0 # FIXME: Should this really be 0?
       
   305 
       
   306         while True:
       
   307             try:
       
   308                 result = command.check_arguments_and_execute(options, args, self)
       
   309                 break
       
   310             except TryAgain, e:
       
   311                 pass
       
   312 
       
   313         self.command_completed()
       
   314         return result