• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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