2009-11-21 Eric Seidel <eric@webkit.org>
[WebKit-https.git] / WebKitTools / Scripts / modules / multicommandtool.py
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 modules.logging import log
39
40 class Command(object):
41     name = None
42     def __init__(self, help_text, argument_names=None, options=None, requires_local_commits=False):
43         self.help_text = help_text
44         self.argument_names = argument_names
45         self.options = options
46         self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options)
47         self.requires_local_commits = requires_local_commits
48
49     def name_with_arguments(self):
50         usage_string = self.name
51         if self.options:
52             usage_string += " [options]"
53         if self.argument_names:
54             usage_string += " " + self.argument_names
55         return usage_string
56
57     def parse_args(self, args):
58         return self.option_parser.parse_args(args)
59
60     def execute(self, options, args, tool):
61         raise NotImplementedError, "subclasses must implement"
62
63 class NonWrappingEpilogIndentedHelpFormatter(IndentedHelpFormatter):
64     # The standard IndentedHelpFormatter paragraph-wraps the epilog, killing our custom formatting.
65     def format_epilog(self, epilog):
66         if epilog:
67             return "\n" + epilog + "\n"
68         return ""
69
70
71 class HelpPrintingOptionParser(OptionParser):
72     def error(self, msg):
73         self.print_usage(sys.stderr)
74         error_message = "%s: error: %s\n" % (self.get_prog_name(), msg)
75         error_message += "\nType \"" + self.get_prog_name() + " --help\" to see usage.\n"
76         self.exit(1, error_message)
77
78
79 class MultiCommandTool(object):
80     def __init__(self, commands=None):
81         # Allow the unit tests to disable command auto-discovery.
82         self.commands = commands or [cls() for cls in self._find_all_commands() if cls.name]
83         # FIXME: Calling self._commands_usage() in the constructor is bad because
84         # it calls self.should_show_command_help which is subclass-defined.
85         # The subclass will not be fully initialized at this point.
86         self.global_option_parser = HelpPrintingOptionParser(usage=self._usage_line(), formatter=NonWrappingEpilogIndentedHelpFormatter(), epilog=self._commands_usage())
87
88     @classmethod
89     def _add_all_subclasses(cls, class_to_crawl, seen_classes):
90         for subclass in class_to_crawl.__subclasses__():
91             if subclass not in seen_classes:
92                 seen_classes.add(subclass)
93                 cls._add_all_subclasses(subclass, seen_classes)
94
95     @classmethod
96     def _find_all_commands(cls):
97         commands = set()
98         cls._add_all_subclasses(Command, commands)
99         return sorted(commands)
100
101     @staticmethod
102     def _usage_line():
103         return "Usage: %prog [options] command [command-options] [command-arguments]"
104
105     def _command_help_formatter(self):
106         # Use our own help formatter so as to indent enough.
107         formatter = IndentedHelpFormatter()
108         formatter.indent()
109         formatter.indent()
110         return formatter
111
112     @classmethod
113     def _help_for_command(cls, command, formatter, longest_name_length):
114         help_text = "  " + command.name_with_arguments().ljust(longest_name_length + 3) + command.help_text + "\n"
115         help_text += command.option_parser.format_option_help(formatter)
116         return help_text
117
118     @classmethod
119     def _standalone_help_for_command(cls, command):
120         return cls._help_for_command(command, IndentedHelpFormatter(), len(command.name_with_arguments()))
121
122     def _commands_usage(self):
123         # Only show commands which are relevant to this checkout.  This might be confusing to some users?
124         relevant_commands = filter(self.should_show_command_help, self.commands)
125         longest_name_length = max(map(lambda command: len(command.name_with_arguments()), relevant_commands))
126         command_help_texts = map(lambda command: self._help_for_command(command, self._command_help_formatter(), longest_name_length), relevant_commands)
127         return "Commands:\n" + "".join(command_help_texts)
128
129     def handle_global_args(self, args):
130         (options, args) = self.global_option_parser.parse_args(args)
131         # We should never hit this because _split_args splits at the first arg without a leading "-".
132         if args:
133             self.global_option_parser.error("Extra arguments before command: " + args)
134
135     @staticmethod
136     def _split_args(args):
137         # Assume the first argument which doesn't start with "-" is the command name.
138         command_index = 0
139         for arg in args:
140             if arg[0] != "-":
141                 break
142             command_index += 1
143         else:
144             return (args[:], None, [])
145
146         global_args = args[:command_index]
147         command = args[command_index]
148         command_args = args[command_index + 1:]
149         return (global_args, command, command_args)
150
151     def command_by_name(self, command_name):
152         for command in self.commands:
153             if command_name == command.name:
154                 return command
155         return None
156
157     def path(self):
158         raise NotImplementedError, "subclasses must implement"
159
160     def should_show_command_help(self, command):
161         raise NotImplementedError, "subclasses must implement"
162
163     def should_execute_command(self, command):
164         raise NotImplementedError, "subclasses must implement"
165
166     def main(self, argv=sys.argv):
167         (global_args, command_name, args_after_command_name) = self._split_args(argv[1:])
168
169         # Handle --help, etc:
170         self.handle_global_args(global_args)
171
172         if not command_name:
173             self.global_option_parser.error("No command specified")
174
175         if command_name == "help":
176             if args_after_command_name:
177                 command = self.command_by_name(args_after_command_name[0])
178                 log(self._standalone_help_for_command(command))
179             else:
180                 self.global_option_parser.print_help()
181             return 0
182
183         command = self.command_by_name(command_name)
184         if not command:
185             self.global_option_parser.error(command_name + " is not a recognized command")
186
187         (should_execute, failure_reason) = self.should_execute_command(command)
188         if not should_execute:
189             log(failure_reason)
190             return 0
191
192         (command_options, command_args) = command.parse_args(args_after_command_name)
193         return command.execute(command_options, command_args, self)