text: #!/usr/bin/env python
  # Copyright (c) 2013 The Chromium Authors. All rights reserved.
  # Use of this source code is governed by a BSD-style license that can be
  # found in the LICENSE file.
  
  """Usage: mffr.py [-d] [-g *.h] [-g *.cc] REGEXP REPLACEMENT
  
  This tool performs a fast find-and-replace operation on files in
  the current git repository.
  
  The -d flag selects a default set of globs (C++ and Objective-C/C++
  source files). The -g flag adds a single glob to the list and may
  be used multiple times. If neither -d nor -g is specified, the tool
  searches all files (*.*).
  
  REGEXP uses full Python regexp syntax. REPLACEMENT can use
  back-references.
  """
  
  from __future__ import print_function
  
  import optparse
  import os
  import re
  import subprocess
  import sys
  
  
  # We can't use shell=True because of the vast and sundry crazy characters we
  # try to pass through to git grep. depot_tools packages a git .bat around
  # a git.cmd around git.exe, which makes it impossible to escape the characters
  # properly. Instead, locate the git .exe up front here. We use cd / && pwd -W,
  # which first changes to the git install root. Inside git bash this "/" is where
  # it hosts a fake /usr, /bin, /etc, ..., but then we use -W to pwd to print the
  # Windows version of the path. Once we have the .exe directly, then we no longer
  # need to use shell=True to subprocess calls, so escaping becomes simply for
  # quotes for CreateProcess(), rather than |, <, >, etc. through multiple layers
  # of cmd.
  if sys.platform == 'win32':
    _git = os.path.normpath(os.path.join(subprocess.check_output(
        'git bash -c "cd / && pwd -W"', shell=True).strip(), 'bin\\git.exe'))
  else:
    _git = 'git'
  
  
  def MultiFileFindReplace(original, replacement, file_globs):
    """Implements fast multi-file find and replace.
  
    Given an |original| string and a |replacement| string, find matching
    files by running git grep on |original| in files matching any
    pattern in |file_globs|.
  
    Once files are found, |re.sub| is run to replace |original| with
    |replacement|.  |replacement| may use capture group back-references.
  
    Args:
      original: '(#(include|import)\s*["<])chrome/browser/ui/browser.h([>"])'
      replacement: '\1chrome/browser/ui/browser/browser.h\3'
      file_globs: ['*.cc', '*.h', '*.m', '*.mm']
  
    Returns the list of files modified.
  
    Raises an exception on error.
    """
    # Posix extended regular expressions do not reliably support the "\s"
    # shorthand.
    posix_ere_original = re.sub(r"\\s", "[[:space:]]", original)
    if sys.platform == 'win32':
      posix_ere_original = posix_ere_original.replace('"', '""')
    out, err = subprocess.Popen(
        [_git, 'grep', '-E', '--name-only', posix_ere_original,
         '--'] + file_globs,
        stdout=subprocess.PIPE).communicate()
    referees = out.splitlines()
  
    for referee in referees:
      with open(referee) as f:
        original_contents = f.read()
      contents = re.sub(original, replacement, original_contents)
      if contents == original_contents:
        raise Exception('No change in file %s although matched in grep' %
                        referee)
      with open(referee, 'wb') as f:
        f.write(contents)
  
    return referees
  
  
  def main():
    parser = optparse.OptionParser(usage='''
  (1) %prog <options> REGEXP REPLACEMENT
  REGEXP uses full Python regexp syntax. REPLACEMENT can use back-references.
  
  (2) %prog <options> -i <file>
  <file> should contain a list (in Python syntax) of
  [REGEXP, REPLACEMENT, [GLOBS]] lists, e.g.:
  [
    [r"(foo|bar)", r"\1baz", ["*.cc", "*.h"]],
    ["54", "42"],
  ]
  As shown above, [GLOBS] can be omitted for a given search-replace list, in which
  case the corresponding search-replace will use the globs specified on the
  command line.''')
    parser.add_option('-d', action='store_true',
                      dest='use_default_glob',
                      help='Perform the change on C++ and Objective-C(++) source '
                      'and header files.')
    parser.add_option('-f', action='store_true',
                      dest='force_unsafe_run',
                      help='Perform the run even if there are uncommitted local '
                      'changes.')
    parser.add_option('-g', action='append',
                      type='string',
                      default=[],
                      metavar="<glob>",
                      dest='user_supplied_globs',
                      help='Perform the change on the specified glob. Can be '
                      'specified multiple times, in which case the globs are '
                      'unioned.')
    parser.add_option('-i', "--input_file",
                      type='string',
                      action='store',
                      default='',
                      metavar="<file>",
                      dest='input_filename',
                      help='Read arguments from <file> rather than the command '
                      'line. NOTE: To be sure of regular expressions being '
                      'interpreted correctly, use raw strings.')
    opts, args = parser.parse_args()
    if opts.use_default_glob and opts.user_supplied_globs:
      print('"-d" and "-g" cannot be used together')
      parser.print_help()
      return 1
  
    from_file = opts.input_filename != ""
    if (from_file and len(args) != 0) or (not from_file and len(args) != 2):
      parser.print_help()
      return 1
  
    if not opts.force_unsafe_run:
      out, err = subprocess.Popen([_git, 'status', '--porcelain'],
                                  stdout=subprocess.PIPE).communicate()
      if out:
        print('ERROR: This tool does not print any confirmation prompts,')
        print('so you should only run it with a clean staging area and cache')
        print('so that reverting a bad find/replace is as easy as running')
        print('  git checkout -- .')
        print('')
        print('To override this safeguard, pass the -f flag.')
        return 1
  
    global_file_globs = ['*.*']
    if opts.use_default_glob:
      global_file_globs = ['*.cc', '*.h', '*.m', '*.mm']
    elif opts.user_supplied_globs:
      global_file_globs = opts.user_supplied_globs
  
    # Construct list of search-replace tasks.
    search_replace_tasks = []
    if opts.input_filename == '':
      original = args[0]
      replacement = args[1]
      search_replace_tasks.append([original, replacement, global_file_globs])
    else:
      f = open(opts.input_filename)
      search_replace_tasks = eval("".join(f.readlines()))
      for task in search_replace_tasks:
        if len(task) == 2:
          task.append(global_file_globs)
      f.close()
  
    for (original, replacement, file_globs) in search_replace_tasks:
      print('File globs:  %s' % file_globs)
      print('Original:    %s' % original)
      print('Replacement: %s' % replacement)
      MultiFileFindReplace(original, replacement, file_globs)
    return 0
  
  
  if __name__ == '__main__':
    sys.exit(main())
  