1#!/usr/bin/env python 2# Copyright (c) 2013 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Usage: mffr.py [-d] [-g *.h] [-g *.cc] REGEXP REPLACEMENT 7 8This tool performs a fast find-and-replace operation on files in 9the current git repository. 10 11The -d flag selects a default set of globs (C++ and Objective-C/C++ 12source files). The -g flag adds a single glob to the list and may 13be used multiple times. If neither -d nor -g is specified, the tool 14searches all files (*.*). 15 16REGEXP uses full Python regexp syntax. REPLACEMENT can use 17back-references. 18""" 19 20from __future__ import print_function 21 22import optparse 23import os 24import re 25import subprocess 26import sys 27 28 29# We can't use shell=True because of the vast and sundry crazy characters we 30# try to pass through to git grep. depot_tools packages a git .bat around 31# a git.cmd around git.exe, which makes it impossible to escape the characters 32# properly. Instead, locate the git .exe up front here. We use cd / && pwd -W, 33# which first changes to the git install root. Inside git bash this "/" is where 34# it hosts a fake /usr, /bin, /etc, ..., but then we use -W to pwd to print the 35# Windows version of the path. Once we have the .exe directly, then we no longer 36# need to use shell=True to subprocess calls, so escaping becomes simply for 37# quotes for CreateProcess(), rather than |, <, >, etc. through multiple layers 38# of cmd. 39if sys.platform == 'win32': 40 _git = os.path.normpath(os.path.join(subprocess.check_output( 41 'git bash -c "cd / && pwd -W"', shell=True).strip(), 'bin\\git.exe')) 42else: 43 _git = 'git' 44 45 46def MultiFileFindReplace(original, replacement, file_globs): 47 """Implements fast multi-file find and replace. 48 49 Given an |original| string and a |replacement| string, find matching 50 files by running git grep on |original| in files matching any 51 pattern in |file_globs|. 52 53 Once files are found, |re.sub| is run to replace |original| with 54 |replacement|. |replacement| may use capture group back-references. 55 56 Args: 57 original: '(#(include|import)\s*["<])chrome/browser/ui/browser.h([>"])' 58 replacement: '\1chrome/browser/ui/browser/browser.h\3' 59 file_globs: ['*.cc', '*.h', '*.m', '*.mm'] 60 61 Returns the list of files modified. 62 63 Raises an exception on error. 64 """ 65 # Posix extended regular expressions do not reliably support the "\s" 66 # shorthand. 67 posix_ere_original = re.sub(r"\\s", "[[:space:]]", original) 68 if sys.platform == 'win32': 69 posix_ere_original = posix_ere_original.replace('"', '""') 70 out, err = subprocess.Popen( 71 [_git, 'grep', '-E', '--name-only', posix_ere_original, 72 '--'] + file_globs, 73 stdout=subprocess.PIPE).communicate() 74 referees = out.splitlines() 75 76 for referee in referees: 77 with open(referee) as f: 78 original_contents = f.read() 79 contents = re.sub(original, replacement, original_contents) 80 if contents == original_contents: 81 raise Exception('No change in file %s although matched in grep' % 82 referee) 83 with open(referee, 'wb') as f: 84 f.write(contents) 85 86 return referees 87 88 89def main(): 90 parser = optparse.OptionParser(usage=''' 91(1) %prog <options> REGEXP REPLACEMENT 92REGEXP uses full Python regexp syntax. REPLACEMENT can use back-references. 93 94(2) %prog <options> -i <file> 95<file> should contain a list (in Python syntax) of 96[REGEXP, REPLACEMENT, [GLOBS]] lists, e.g.: 97[ 98 [r"(foo|bar)", r"\1baz", ["*.cc", "*.h"]], 99 ["54", "42"], 100] 101As shown above, [GLOBS] can be omitted for a given search-replace list, in which 102case the corresponding search-replace will use the globs specified on the 103command line.''') 104 parser.add_option('-d', action='store_true', 105 dest='use_default_glob', 106 help='Perform the change on C++ and Objective-C(++) source ' 107 'and header files.') 108 parser.add_option('-f', action='store_true', 109 dest='force_unsafe_run', 110 help='Perform the run even if there are uncommitted local ' 111 'changes.') 112 parser.add_option('-g', action='append', 113 type='string', 114 default=[], 115 metavar="<glob>", 116 dest='user_supplied_globs', 117 help='Perform the change on the specified glob. Can be ' 118 'specified multiple times, in which case the globs are ' 119 'unioned.') 120 parser.add_option('-i', "--input_file", 121 type='string', 122 action='store', 123 default='', 124 metavar="<file>", 125 dest='input_filename', 126 help='Read arguments from <file> rather than the command ' 127 'line. NOTE: To be sure of regular expressions being ' 128 'interpreted correctly, use raw strings.') 129 opts, args = parser.parse_args() 130 if opts.use_default_glob and opts.user_supplied_globs: 131 print('"-d" and "-g" cannot be used together') 132 parser.print_help() 133 return 1 134 135 from_file = opts.input_filename != "" 136 if (from_file and len(args) != 0) or (not from_file and len(args) != 2): 137 parser.print_help() 138 return 1 139 140 if not opts.force_unsafe_run: 141 out, err = subprocess.Popen([_git, 'status', '--porcelain'], 142 stdout=subprocess.PIPE).communicate() 143 if out: 144 print('ERROR: This tool does not print any confirmation prompts,') 145 print('so you should only run it with a clean staging area and cache') 146 print('so that reverting a bad find/replace is as easy as running') 147 print(' git checkout -- .') 148 print('') 149 print('To override this safeguard, pass the -f flag.') 150 return 1 151 152 global_file_globs = ['*.*'] 153 if opts.use_default_glob: 154 global_file_globs = ['*.cc', '*.h', '*.m', '*.mm'] 155 elif opts.user_supplied_globs: 156 global_file_globs = opts.user_supplied_globs 157 158 # Construct list of search-replace tasks. 159 search_replace_tasks = [] 160 if opts.input_filename == '': 161 original = args[0] 162 replacement = args[1] 163 search_replace_tasks.append([original, replacement, global_file_globs]) 164 else: 165 f = open(opts.input_filename) 166 search_replace_tasks = eval("".join(f.readlines())) 167 for task in search_replace_tasks: 168 if len(task) == 2: 169 task.append(global_file_globs) 170 f.close() 171 172 for (original, replacement, file_globs) in search_replace_tasks: 173 print('File globs: %s' % file_globs) 174 print('Original: %s' % original) 175 print('Replacement: %s' % replacement) 176 MultiFileFindReplace(original, replacement, file_globs) 177 return 0 178 179 180if __name__ == '__main__': 181 sys.exit(main()) 182