OLD | NEW |
(Empty) | |
| 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 |
| 8 This tool performs a fast find-and-replace operation on files in |
| 9 the current git repository. |
| 10 |
| 11 The -d flag selects a default set of globs (C++ and Objective-C/C++ |
| 12 source files). The -g flag adds a single glob to the list and may |
| 13 be used multiple times. If neither -d nor -g is specified, the tool |
| 14 searches all files (*.*). |
| 15 |
| 16 REGEXP uses full Python regexp syntax. REPLACEMENT can use |
| 17 back-references. |
| 18 """ |
| 19 |
| 20 import optparse |
| 21 import re |
| 22 import subprocess |
| 23 import 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 |
| 32 def 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 |
| 76 def main(): |
| 77 parser = optparse.OptionParser(usage=''' |
| 78 (1) %prog <options> REGEXP REPLACEMENT |
| 79 REGEXP 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 ] |
| 88 As shown above, [GLOBS] can be omitted for a given search-replace list, in which |
| 89 case the corresponding search-replace will use the globs specified on the |
| 90 command 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 |
| 168 if __name__ == '__main__': |
| 169 sys.exit(main()) |
OLD | NEW |