1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
|
#!/usr/bin/env python3
# Copyright 2013 The Chromium Authors
# 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):
r"""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())
|