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.
6 """Usage: mffr.py [-d] [-g *.h] [-g *.cc] REGEXP REPLACEMENT
8 This tool performs a fast find-and-replace operation on files in
9 the current git repository.
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 (*.*).
16 REGEXP uses full Python regexp syntax. REPLACEMENT can use
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
29 _USE_SHELL
= (sys
.platform
== 'win32')
32 def MultiFileFindReplace(original
, replacement
, file_globs
):
33 """Implements fast multi-file find and replace.
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|.
39 Once files are found, |re.sub| is run to replace |original| with
40 |replacement|. |replacement| may use capture group back-references.
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']
47 Returns the list of files modified.
49 Raises an exception on error.
51 # Posix extended regular expressions do not reliably support the "\s"
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
,
59 stdout
=subprocess
.PIPE
,
60 shell
=_USE_SHELL
).communicate()
61 referees
= out
.splitlines()
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' %
70 with
open(referee
, 'wb') as f
:
77 parser
= optparse
.OptionParser(usage
='''
78 (1) %prog <options> REGEXP REPLACEMENT
79 REGEXP uses full Python regexp syntax. REPLACEMENT can use back-references.
81 (2) %prog <options> -i <file>
82 <file> should contain a list (in Python syntax) of
83 [REGEXP, REPLACEMENT, [GLOBS]] lists, e.g.:
85 [r"(foo|bar)", r"\1baz", ["*.cc", "*.h"]],
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
91 parser
.add_option('-d', action
='store_true',
92 dest
='use_default_glob',
93 help='Perform the change on C++ and Objective-C(++) source '
95 parser
.add_option('-f', action
='store_true',
96 dest
='force_unsafe_run',
97 help='Perform the run even if there are uncommitted local '
99 parser
.add_option('-g', action
='append',
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 '
107 parser
.add_option('-i', "--input_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'
122 from_file
= opts
.input_filename
!= ""
123 if (from_file
and len(args
) != 0) or (not from_file
and len(args
) != 2):
127 if not opts
.force_unsafe_run
:
128 out
, err
= subprocess
.Popen(['git', 'status', '--porcelain'],
129 stdout
=subprocess
.PIPE
,
130 shell
=_USE_SHELL
).communicate()
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 -- .'
137 print 'To override this safeguard, pass the -f flag.'
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
146 # Construct list of search-replace tasks.
147 search_replace_tasks
= []
148 if opts
.input_filename
== '':
150 replacement
= args
[1]
151 search_replace_tasks
.append([original
, replacement
, global_file_globs
])
153 f
= open(opts
.input_filename
)
154 search_replace_tasks
= eval("".join(f
.readlines()))
155 for task
in search_replace_tasks
:
157 task
.append(global_file_globs
)
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
)
168 if __name__
== '__main__':