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 20import optparse 21import re 22import subprocess 23import sys 24 25 26# We need to use shell=True with subprocess on Windows so that it 27# finds 'git' from the path, but can lead to undesired behavior on 28# Linux. 29_USE_SHELL = (sys.platform == 'win32') 30 31 32def MultiFileFindReplace(original, replacement, file_globs): 33 """Implements fast multi-file find and replace. 34 35 Given an |original| string and a |replacement| string, find matching 36 files by running git grep on |original| in files matching any 37 pattern in |file_globs|. 38 39 Once files are found, |re.sub| is run to replace |original| with 40 |replacement|. |replacement| may use capture group back-references. 41 42 Args: 43 original: '(#(include|import)\s*["<])chrome/browser/ui/browser.h([>"])' 44 replacement: '\1chrome/browser/ui/browser/browser.h\3' 45 file_globs: ['*.cc', '*.h', '*.m', '*.mm'] 46 47 Returns the list of files modified. 48 49 Raises an exception on error. 50 """ 51 # Posix extended regular expressions do not reliably support the "\s" 52 # shorthand. 53 posix_ere_original = re.sub(r"\\s", "[[:space:]]", original) 54 if sys.platform == 'win32': 55 posix_ere_original = posix_ere_original.replace('"', '""') 56 out, err = subprocess.Popen( 57 ['git', 'grep', '-E', '--name-only', posix_ere_original, 58 '--'] + file_globs, 59 stdout=subprocess.PIPE, 60 shell=_USE_SHELL).communicate() 61 referees = out.splitlines() 62 63 for referee in referees: 64 with open(referee) as f: 65 original_contents = f.read() 66 contents = re.sub(original, replacement, original_contents) 67 if contents == original_contents: 68 raise Exception('No change in file %s although matched in grep' % 69 referee) 70 with open(referee, 'wb') as f: 71 f.write(contents) 72 73 return referees 74 75 76def main(): 77 parser = optparse.OptionParser(usage=''' 78(1) %prog <options> REGEXP REPLACEMENT 79REGEXP uses full Python regexp syntax. REPLACEMENT can use back-references. 80 81(2) %prog <options> -i <file> 82<file> should contain a list (in Python syntax) of 83[REGEXP, REPLACEMENT, [GLOBS]] lists, e.g.: 84[ 85 [r"(foo|bar)", r"\1baz", ["*.cc", "*.h"]], 86 ["54", "42"], 87] 88As shown above, [GLOBS] can be omitted for a given search-replace list, in which 89case the corresponding search-replace will use the globs specified on the 90command line.''') 91 parser.add_option('-d', action='store_true', 92 dest='use_default_glob', 93 help='Perform the change on C++ and Objective-C(++) source ' 94 'and header files.') 95 parser.add_option('-f', action='store_true', 96 dest='force_unsafe_run', 97 help='Perform the run even if there are uncommitted local ' 98 'changes.') 99 parser.add_option('-g', action='append', 100 type='string', 101 default=[], 102 metavar="<glob>", 103 dest='user_supplied_globs', 104 help='Perform the change on the specified glob. Can be ' 105 'specified multiple times, in which case the globs are ' 106 'unioned.') 107 parser.add_option('-i', "--input_file", 108 type='string', 109 action='store', 110 default='', 111 metavar="<file>", 112 dest='input_filename', 113 help='Read arguments from <file> rather than the command ' 114 'line. NOTE: To be sure of regular expressions being ' 115 'interpreted correctly, use raw strings.') 116 opts, args = parser.parse_args() 117 if opts.use_default_glob and opts.user_supplied_globs: 118 print '"-d" and "-g" cannot be used together' 119 parser.print_help() 120 return 1 121 122 from_file = opts.input_filename != "" 123 if (from_file and len(args) != 0) or (not from_file and len(args) != 2): 124 parser.print_help() 125 return 1 126 127 if not opts.force_unsafe_run: 128 out, err = subprocess.Popen(['git', 'status', '--porcelain'], 129 stdout=subprocess.PIPE, 130 shell=_USE_SHELL).communicate() 131 if out: 132 print 'ERROR: This tool does not print any confirmation prompts,' 133 print 'so you should only run it with a clean staging area and cache' 134 print 'so that reverting a bad find/replace is as easy as running' 135 print ' git checkout -- .' 136 print '' 137 print 'To override this safeguard, pass the -f flag.' 138 return 1 139 140 global_file_globs = ['*.*'] 141 if opts.use_default_glob: 142 global_file_globs = ['*.cc', '*.h', '*.m', '*.mm'] 143 elif opts.user_supplied_globs: 144 global_file_globs = opts.user_supplied_globs 145 146 # Construct list of search-replace tasks. 147 search_replace_tasks = [] 148 if opts.input_filename == '': 149 original = args[0] 150 replacement = args[1] 151 search_replace_tasks.append([original, replacement, global_file_globs]) 152 else: 153 f = open(opts.input_filename) 154 search_replace_tasks = eval("".join(f.readlines())) 155 for task in search_replace_tasks: 156 if len(task) == 2: 157 task.append(global_file_globs) 158 f.close() 159 160 for (original, replacement, file_globs) in search_replace_tasks: 161 print 'File globs: %s' % file_globs 162 print 'Original: %s' % original 163 print 'Replacement: %s' % replacement 164 MultiFileFindReplace(original, replacement, file_globs) 165 return 0 166 167 168if __name__ == '__main__': 169 sys.exit(main()) 170