Bug 795491 - Improve mach's help text; r=jhammel
authorGregory Szorc <gps@mozilla.com>
Mon, 01 Oct 2012 11:06:40 -0700
changeset 108838 573907d351e3e1d0a51dba223be15b76aa92fdeb
parent 108837 9e8a91dfbc7e07bf80fcaba5db3aeb04ebee4989
child 108839 98d8b5c9bafbd55b47ba0b9984e3575ef27e5b98
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewersjhammel
bugs795491
milestone18.0a1
Bug 795491 - Improve mach's help text; r=jhammel
python/mach/mach/main.py
--- a/python/mach/mach/main.py
+++ b/python/mach/mach/main.py
@@ -40,36 +40,77 @@ SETTINGS_PROVIDERS = [
 
 # Settings for argument parser that don't get proxied to sub-module. i.e. these
 # are things consumed by the driver itself.
 CONSUMED_ARGUMENTS = [
     'settings_file',
     'verbose',
     'logfile',
     'log_interval',
-    'action',
+    'command',
     'cls',
     'method',
     'func',
 ]
 
+
+class ArgumentParser(argparse.ArgumentParser):
+    """Custom implementation argument parser to make things look pretty."""
+
+    def error(self, message):
+        """Custom error reporter to give more helpful text on bad commands."""
+        if not message.startswith('argument command: invalid choice'):
+            argparse.ArgumentParser.error(self, message)
+            assert False
+
+        print('Invalid command specified. The list of commands is below.\n')
+        self.print_help()
+        sys.exit(1)
+
+    def format_help(self):
+        text = argparse.ArgumentParser.format_help(self)
+
+        # Strip out the silly command list that would preceed the pretty list.
+        #
+        # Commands:
+        #   {foo,bar}
+        #     foo  Do foo.
+        #     bar  Do bar.
+        search = 'Commands:\n  {'
+        start = text.find(search)
+
+        if start != -1:
+            end = text.find('}\n', start)
+            assert end != -1
+
+            real_start = start + len('Commands:\n')
+            real_end = end + len('}\n')
+
+            text = text[0:real_start] + text[real_end:]
+
+        return text
+
+
 class Mach(object):
     """Contains code for the command-line `mach` interface."""
 
-    USAGE = """%(prog)s subcommand [arguments]
+    USAGE = """%(prog)s [global arguments] command [command arguments]
 
-mach provides an interface to performing common developer tasks. You specify
-an action/sub-command and it performs it.
+mach (German for "do") is the main interface to the Mozilla build system and
+common developer tasks.
+
+You tell mach the command you want to perform and it does it for you.
 
-Some common actions are:
+Some common commands are:
 
+    %(prog)s build     Build/compile the source tree.
+    %(prog)s test      Run tests.
     %(prog)s help      Show full help, including the list of all commands.
-    %(prog)s test      Run tests.
 
-To see more help for a specific action, run:
+To see more help for a specific command, run:
 
   %(prog)s <command> --help
 """
 
     def __init__(self, cwd):
         assert os.path.isdir(cwd)
 
         self.cwd = cwd
@@ -112,24 +153,24 @@ To see more help for a specific action, 
             write_interval=args.log_interval)
 
         self.load_settings(args)
         conf = BuildConfig(self.settings)
 
         stripped = {k: getattr(args, k) for k in vars(args) if k not in
             CONSUMED_ARGUMENTS}
 
-        # If the action is associated with a class, instantiate and run it.
+        # If the command is associated with a class, instantiate and run it.
         # All classes must be Base-derived and take the expected argument list.
         if hasattr(args, 'cls'):
             cls = getattr(args, 'cls')
             instance = cls(self.cwd, self.settings, self.log_manager)
             fn = getattr(instance, getattr(args, 'method'))
 
-        # If the action is associated with a function, call it.
+        # If the command is associated with a function, call it.
         elif hasattr(args, 'func'):
             fn = getattr(args, 'func')
         else:
             raise Exception('Dispatch configuration error in module.')
 
         fn(**stripped)
 
     def log(self, level, action, params, format_str):
@@ -168,34 +209,41 @@ To see more help for a specific action, 
 
         self.settings.load_file(p)
 
         return os.path.exists(p)
 
     def get_argument_parser(self):
         """Returns an argument parser for the command-line interface."""
 
-        parser = argparse.ArgumentParser()
+        parser = ArgumentParser(add_help=False,
+            usage='%(prog)s [global arguments] command [command arguments]')
 
-        settings_group = parser.add_argument_group('Settings')
-        settings_group.add_argument('--settings', dest='settings_file',
+        # Order is important here as it dictates the order the auto-generated
+        # help messages are printed.
+        subparser = parser.add_subparsers(dest='command', title='Commands')
+        parser.set_defaults(command='help')
+
+        global_group = parser.add_argument_group('Global Arguments')
+
+        global_group.add_argument('-h', '--help', action='help',
+            help='Show this help message and exit.')
+
+        global_group.add_argument('--settings', dest='settings_file',
             metavar='FILENAME', help='Path to settings file.')
 
-        logging_group = parser.add_argument_group('Logging')
-        logging_group.add_argument('-v', '--verbose', dest='verbose',
+        global_group.add_argument('-v', '--verbose', dest='verbose',
             action='store_true', default=False,
             help='Print verbose output.')
-        logging_group.add_argument('-l', '--log-file', dest='logfile',
+        global_group.add_argument('-l', '--log-file', dest='logfile',
             metavar='FILENAME', type=argparse.FileType('ab'),
             help='Filename to write log data to.')
-        logging_group.add_argument('--log-interval', dest='log_interval',
+        global_group.add_argument('--log-interval', dest='log_interval',
             action='store_true', default=False,
             help='Prefix log line with interval from last message rather '
                 'than relative time. Note that this is NOT execution time '
                 'if there are parallel operations.')
 
-        subparser = parser.add_subparsers(dest='action')
-
         # Register argument action providers with us.
         for cls in HANDLERS:
             cls.populate_argparse(subparser)
 
         return parser