OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Usage: mffr.py [-d] [-g *.h] [-g *.cc] REGEXP REPLACEMENT | 6 """Usage: mffr.py [-d] [-g *.h] [-g *.cc] REGEXP REPLACEMENT |
7 | 7 |
8 This tool performs a fast find-and-replace operation on files in | 8 This tool performs a fast find-and-replace operation on files in |
9 the current git repository. | 9 the current git repository. |
10 | 10 |
11 The -d flag selects a default set of globs (C++ and Objective-C/C++ | 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 | 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 | 13 be used multiple times. If neither -d nor -g is specified, the tool |
14 searches all files (*.*). | 14 searches all files (*.*). |
15 | 15 |
16 REGEXP uses full Python regexp syntax. REPLACEMENT can use | 16 REGEXP uses full Python regexp syntax. REPLACEMENT can use |
17 back-references. | 17 back-references. |
18 """ | 18 """ |
19 | 19 |
20 import optparse | 20 import optparse |
| 21 import os |
21 import re | 22 import re |
22 import subprocess | 23 import subprocess |
23 import sys | 24 import sys |
24 | 25 |
25 | 26 |
26 # We need to use shell=True with subprocess on Windows so that it | 27 # We can't use shell=True because of the vast and sundry crazy characters we |
27 # finds 'git' from the path, but can lead to undesired behavior on | 28 # try to pass through to git grep. depot_tools packages a git .bat around |
28 # Linux. | 29 # a git.cmd around git.exe, which makes it impossible to escape the characters |
29 _USE_SHELL = (sys.platform == 'win32') | 30 # properly. Instead, locate the git .exe up front here. We use cd / && pwd -W, |
| 31 # which first changes to the git install root. Inside git bash this "/" is where |
| 32 # it hosts a fake /usr, /bin, /etc, ..., but then we use -W to pwd to print the |
| 33 # Windows version of the path. Once we have the .exe directly, then we no longer |
| 34 # need to use shell=True to subprocess calls, so escaping becomes simply for |
| 35 # quotes for CreateProcess(), rather than |, <, >, etc. through multiple layers |
| 36 # of cmd. |
| 37 if sys.platform == 'win32': |
| 38 _git = os.path.normpath(os.path.join(subprocess.check_output( |
| 39 'git bash -c "cd / && pwd -W"', shell=True).strip(), 'bin\\git.exe')) |
| 40 else: |
| 41 _git = 'git' |
30 | 42 |
31 | 43 |
32 def MultiFileFindReplace(original, replacement, file_globs): | 44 def MultiFileFindReplace(original, replacement, file_globs): |
33 """Implements fast multi-file find and replace. | 45 """Implements fast multi-file find and replace. |
34 | 46 |
35 Given an |original| string and a |replacement| string, find matching | 47 Given an |original| string and a |replacement| string, find matching |
36 files by running git grep on |original| in files matching any | 48 files by running git grep on |original| in files matching any |
37 pattern in |file_globs|. | 49 pattern in |file_globs|. |
38 | 50 |
39 Once files are found, |re.sub| is run to replace |original| with | 51 Once files are found, |re.sub| is run to replace |original| with |
40 |replacement|. |replacement| may use capture group back-references. | 52 |replacement|. |replacement| may use capture group back-references. |
41 | 53 |
42 Args: | 54 Args: |
43 original: '(#(include|import)\s*["<])chrome/browser/ui/browser.h([>"])' | 55 original: '(#(include|import)\s*["<])chrome/browser/ui/browser.h([>"])' |
44 replacement: '\1chrome/browser/ui/browser/browser.h\3' | 56 replacement: '\1chrome/browser/ui/browser/browser.h\3' |
45 file_globs: ['*.cc', '*.h', '*.m', '*.mm'] | 57 file_globs: ['*.cc', '*.h', '*.m', '*.mm'] |
46 | 58 |
47 Returns the list of files modified. | 59 Returns the list of files modified. |
48 | 60 |
49 Raises an exception on error. | 61 Raises an exception on error. |
50 """ | 62 """ |
51 # Posix extended regular expressions do not reliably support the "\s" | 63 # Posix extended regular expressions do not reliably support the "\s" |
52 # shorthand. | 64 # shorthand. |
53 posix_ere_original = re.sub(r"\\s", "[[:space:]]", original) | 65 posix_ere_original = re.sub(r"\\s", "[[:space:]]", original) |
54 if sys.platform == 'win32': | 66 if sys.platform == 'win32': |
55 posix_ere_original = posix_ere_original.replace('"', '""') | 67 posix_ere_original = posix_ere_original.replace('"', '""') |
56 out, err = subprocess.Popen( | 68 out, err = subprocess.Popen( |
57 ['git', 'grep', '-E', '--name-only', posix_ere_original, | 69 [_git, 'grep', '-E', '--name-only', posix_ere_original, |
58 '--'] + file_globs, | 70 '--'] + file_globs, |
59 stdout=subprocess.PIPE, | 71 stdout=subprocess.PIPE).communicate() |
60 shell=_USE_SHELL).communicate() | |
61 referees = out.splitlines() | 72 referees = out.splitlines() |
62 | 73 |
63 for referee in referees: | 74 for referee in referees: |
64 with open(referee) as f: | 75 with open(referee) as f: |
65 original_contents = f.read() | 76 original_contents = f.read() |
66 contents = re.sub(original, replacement, original_contents) | 77 contents = re.sub(original, replacement, original_contents) |
67 if contents == original_contents: | 78 if contents == original_contents: |
68 raise Exception('No change in file %s although matched in grep' % | 79 raise Exception('No change in file %s although matched in grep' % |
69 referee) | 80 referee) |
70 with open(referee, 'wb') as f: | 81 with open(referee, 'wb') as f: |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
118 print '"-d" and "-g" cannot be used together' | 129 print '"-d" and "-g" cannot be used together' |
119 parser.print_help() | 130 parser.print_help() |
120 return 1 | 131 return 1 |
121 | 132 |
122 from_file = opts.input_filename != "" | 133 from_file = opts.input_filename != "" |
123 if (from_file and len(args) != 0) or (not from_file and len(args) != 2): | 134 if (from_file and len(args) != 0) or (not from_file and len(args) != 2): |
124 parser.print_help() | 135 parser.print_help() |
125 return 1 | 136 return 1 |
126 | 137 |
127 if not opts.force_unsafe_run: | 138 if not opts.force_unsafe_run: |
128 out, err = subprocess.Popen(['git', 'status', '--porcelain'], | 139 out, err = subprocess.Popen([_git, 'status', '--porcelain'], |
129 stdout=subprocess.PIPE, | 140 stdout=subprocess.PIPE).communicate() |
130 shell=_USE_SHELL).communicate() | |
131 if out: | 141 if out: |
132 print 'ERROR: This tool does not print any confirmation prompts,' | 142 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' | 143 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' | 144 print 'so that reverting a bad find/replace is as easy as running' |
135 print ' git checkout -- .' | 145 print ' git checkout -- .' |
136 print '' | 146 print '' |
137 print 'To override this safeguard, pass the -f flag.' | 147 print 'To override this safeguard, pass the -f flag.' |
138 return 1 | 148 return 1 |
139 | 149 |
140 global_file_globs = ['*.*'] | 150 global_file_globs = ['*.*'] |
(...skipping 19 matching lines...) Expand all Loading... |
160 for (original, replacement, file_globs) in search_replace_tasks: | 170 for (original, replacement, file_globs) in search_replace_tasks: |
161 print 'File globs: %s' % file_globs | 171 print 'File globs: %s' % file_globs |
162 print 'Original: %s' % original | 172 print 'Original: %s' % original |
163 print 'Replacement: %s' % replacement | 173 print 'Replacement: %s' % replacement |
164 MultiFileFindReplace(original, replacement, file_globs) | 174 MultiFileFindReplace(original, replacement, file_globs) |
165 return 0 | 175 return 0 |
166 | 176 |
167 | 177 |
168 if __name__ == '__main__': | 178 if __name__ == '__main__': |
169 sys.exit(main()) | 179 sys.exit(main()) |
OLD | NEW |