Update argument parsing to use argparse

This CL updates argument parsing mechanism from using a custom implementation + optparse to argparse. It is intended to clear tech debt for future changes.

Change-Id: I06ba5d3c532638a7970be960e02ebbafbea9dcb0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/3541479
Reviewed-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Josip Sokcevic <sokcevic@google.com>
Commit-Queue: Aravind Vasudevan <aravindvasudev@google.com>
diff --git a/fetch.py b/fetch.py
index 2b7245d..f19f103 100755
--- a/fetch.py
+++ b/fetch.py
@@ -21,7 +21,7 @@
 from __future__ import print_function
 
 import json
-import optparse
+import argparse
 import os
 import pipes
 import subprocess
@@ -174,85 +174,48 @@
     raise KeyError('unrecognized checkout type: %s' % type_name)
   return class_(options, spec, root)
 
-
-#################################################
-# Utility function and file entry point.
-#################################################
-def usage(msg=None):
-  """Print help and exit."""
-  if msg:
-    print('Error:', msg)
-
-  print(textwrap.dedent("""\
-    usage: %s [options] <config> [--property=value [--property2=value2 ...]]
-
-    This script can be used to download the Chromium sources. See
-    http://www.chromium.org/developers/how-tos/get-the-code
-    for full usage instructions.
-
-    Valid options:
-       -h, --help, help   Print this message.
-       --nohooks          Don't run hooks after checkout.
-       --force            (dangerous) Don't look for existing .gclient file.
-       -n, --dry-run      Don't run commands, only print them.
-       --no-history       Perform shallow clones, don't fetch the full git history.
-
-    Valid fetch configs:""") % os.path.basename(sys.argv[0]))
+def handle_args(argv):
+  """Gets the config name from the command line arguments."""
 
   configs_dir = os.path.join(SCRIPT_PATH, 'fetch_configs')
   configs = [f[:-3] for f in os.listdir(configs_dir) if f.endswith('.py')]
   configs.sort()
-  for fname in configs:
-    print('  ' + fname)
 
-  sys.exit(bool(msg))
+  parser = argparse.ArgumentParser(
+    formatter_class=argparse.RawDescriptionHelpFormatter,
+    description='''
+    This script can be used to download the Chromium sources. See
+    http://www.chromium.org/developers/how-tos/get-the-code
+    for full usage instructions.''',
+    epilog='Valid fetch configs:\n' + \
+      '\n'.join(map(lambda s: '  ' + s, configs))
+    )
 
+  parser.add_argument('-n', '--dry-run', action='store_true', default=False,
+    help='Don\'t run commands, only print them.')
+  parser.add_argument('--nohooks', action='store_true', default=False,
+    help='Don\'t run hooks after checkout.')
+  parser.add_argument('--no-history', action='store_true', default=False,
+    help='Perform shallow clones, don\'t fetch the full git history.')
+  parser.add_argument('--force', action='store_true', default=False,
+    help='(dangerous) Don\'t look for existing .gclient file.')
 
-def handle_args(argv):
-  """Gets the config name from the command line arguments."""
-  if len(argv) <= 1:
-    usage('Must specify a config.')
-  if argv[1] in ('-h', '--help', 'help'):
-    usage()
+  parser.add_argument('config', type=str,
+    help="Project to fetch, e.g. chromium.")
+  parser.add_argument('props', metavar='props', type=str,
+    nargs=argparse.REMAINDER, default=[])
 
-  dry_run = False
-  nohooks = False
-  no_history = False
-  force = False
-  while len(argv) >= 2:
-    arg = argv[1]
-    if not arg.startswith('-'):
-      break
-    argv.pop(1)
-    if arg in ('-n', '--dry-run'):
-      dry_run = True
-    elif arg == '--nohooks':
-      nohooks = True
-    elif arg == '--no-history':
-      no_history = True
-    elif arg == '--force':
-      force = True
-    else:
-      usage('Invalid option %s.' % arg)
+  args = parser.parse_args(argv[1:])
 
-  def looks_like_arg(arg):
-    return arg.startswith('--') and arg.count('=') == 1
+  # props passed to config must be of the format --<name>=<value>
+  looks_like_arg = lambda arg: arg.startswith('--') and arg.count('=') == 1
+  bad_param = [x for x in args.props if not looks_like_arg(x)]
+  if bad_param:
+    print('Error: Got bad arguments %s' % bad_param)
+    parser.print_help()
+    sys.exit(1)
 
-  bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
-  if bad_parms:
-    usage('Got bad arguments %s' % bad_parms)
-
-  config = argv[1]
-  props = argv[2:]
-  return (
-      optparse.Values({
-        'dry_run': dry_run,
-        'nohooks': nohooks,
-        'no_history': no_history,
-        'force': force}),
-      config,
-      props)
-
+  return args
 
 def run_config_fetch(config, props, aliased=False):
   """Invoke a config's fetch method with the passed-through args
@@ -305,9 +268,9 @@
 
 
 def main():
-  options, config, props = handle_args(sys.argv)
-  spec, root = run_config_fetch(config, props)
-  return run(options, spec, root)
+  args = handle_args(sys.argv)
+  spec, root = run_config_fetch(args.config, args.props)
+  return run(args, spec, root)
 
 
 if __name__ == '__main__':