123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- #!/usr/bin/env python3
- # 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).decode('utf-8').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, encoding='utf-8') 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, mode='w', encoding='utf-8', newline='\n') 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())
|