Bug 795491 - Improve mach's help text; r=jhammel
authorGregory Szorc <gps@mozilla.com>
Mon, 01 Oct 2012 11:06:40 -0700
changeset 108729 573907d351e3e1d0a51dba223be15b76aa92fdeb
parent 108728 9e8a91dfbc7e07bf80fcaba5db3aeb04ebee4989
child 108730 98d8b5c9bafbd55b47ba0b9984e3575ef27e5b98
push id23587
push usergszorc@mozilla.com
push dateMon, 01 Oct 2012 18:13:45 +0000
treeherdermozilla-central@85df971e0db1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjhammel
bugs795491
milestone18.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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