blob: b260f149d472b68da69d4dcf7d7d8a03c04b0bb9 [file] [log] [blame]
#!/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.
"""
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())