mffr.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. #!/usr/bin/env python3
  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. """Usage: mffr.py [-d] [-g *.h] [-g *.cc] REGEXP REPLACEMENT
  6. This tool performs a fast find-and-replace operation on files in
  7. the current git repository.
  8. The -d flag selects a default set of globs (C++ and Objective-C/C++
  9. source files). The -g flag adds a single glob to the list and may
  10. be used multiple times. If neither -d nor -g is specified, the tool
  11. searches all files (*.*).
  12. REGEXP uses full Python regexp syntax. REPLACEMENT can use
  13. back-references.
  14. """
  15. from __future__ import print_function
  16. import optparse
  17. import os
  18. import re
  19. import subprocess
  20. import sys
  21. # We can't use shell=True because of the vast and sundry crazy characters we
  22. # try to pass through to git grep. depot_tools packages a git .bat around
  23. # a git.cmd around git.exe, which makes it impossible to escape the characters
  24. # properly. Instead, locate the git .exe up front here. We use cd / && pwd -W,
  25. # which first changes to the git install root. Inside git bash this "/" is where
  26. # it hosts a fake /usr, /bin, /etc, ..., but then we use -W to pwd to print the
  27. # Windows version of the path. Once we have the .exe directly, then we no longer
  28. # need to use shell=True to subprocess calls, so escaping becomes simply for
  29. # quotes for CreateProcess(), rather than |, <, >, etc. through multiple layers
  30. # of cmd.
  31. if sys.platform == 'win32':
  32. _git = os.path.normpath(
  33. os.path.join(
  34. subprocess.check_output('git bash -c "cd / && pwd -W"',
  35. shell=True).decode('utf-8').strip(),
  36. 'bin\\git.exe'))
  37. else:
  38. _git = 'git'
  39. def MultiFileFindReplace(original, replacement, file_globs):
  40. """Implements fast multi-file find and replace.
  41. Given an |original| string and a |replacement| string, find matching
  42. files by running git grep on |original| in files matching any
  43. pattern in |file_globs|.
  44. Once files are found, |re.sub| is run to replace |original| with
  45. |replacement|. |replacement| may use capture group back-references.
  46. Args:
  47. original: '(#(include|import)\s*["<])chrome/browser/ui/browser.h([>"])'
  48. replacement: '\1chrome/browser/ui/browser/browser.h\3'
  49. file_globs: ['*.cc', '*.h', '*.m', '*.mm']
  50. Returns the list of files modified.
  51. Raises an exception on error.
  52. """
  53. # Posix extended regular expressions do not reliably support the "\s"
  54. # shorthand.
  55. posix_ere_original = re.sub(r"\\s", "[[:space:]]", original)
  56. if sys.platform == 'win32':
  57. posix_ere_original = posix_ere_original.replace('"', '""')
  58. out, err = subprocess.Popen(
  59. [_git, 'grep', '-E', '--name-only', posix_ere_original,
  60. '--'] + file_globs,
  61. stdout=subprocess.PIPE).communicate()
  62. referees = out.splitlines()
  63. for referee in referees:
  64. with open(referee, encoding='utf-8') 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, mode='w', encoding='utf-8', newline='\n') as f:
  71. f.write(contents)
  72. return referees
  73. def main():
  74. parser = optparse.OptionParser(usage='''
  75. (1) %prog <options> REGEXP REPLACEMENT
  76. REGEXP uses full Python regexp syntax. REPLACEMENT can use back-references.
  77. (2) %prog <options> -i <file>
  78. <file> should contain a list (in Python syntax) of
  79. [REGEXP, REPLACEMENT, [GLOBS]] lists, e.g.:
  80. [
  81. [r"(foo|bar)", r"\1baz", ["*.cc", "*.h"]],
  82. ["54", "42"],
  83. ]
  84. As shown above, [GLOBS] can be omitted for a given search-replace list, in which
  85. case the corresponding search-replace will use the globs specified on the
  86. command line.''')
  87. parser.add_option('-d', action='store_true',
  88. dest='use_default_glob',
  89. help='Perform the change on C++ and Objective-C(++) source '
  90. 'and header files.')
  91. parser.add_option('-f', action='store_true',
  92. dest='force_unsafe_run',
  93. help='Perform the run even if there are uncommitted local '
  94. 'changes.')
  95. parser.add_option('-g', action='append',
  96. type='string',
  97. default=[],
  98. metavar="<glob>",
  99. dest='user_supplied_globs',
  100. help='Perform the change on the specified glob. Can be '
  101. 'specified multiple times, in which case the globs are '
  102. 'unioned.')
  103. parser.add_option('-i', "--input_file",
  104. type='string',
  105. action='store',
  106. default='',
  107. metavar="<file>",
  108. dest='input_filename',
  109. help='Read arguments from <file> rather than the command '
  110. 'line. NOTE: To be sure of regular expressions being '
  111. 'interpreted correctly, use raw strings.')
  112. opts, args = parser.parse_args()
  113. if opts.use_default_glob and opts.user_supplied_globs:
  114. print('"-d" and "-g" cannot be used together')
  115. parser.print_help()
  116. return 1
  117. from_file = opts.input_filename != ""
  118. if (from_file and len(args) != 0) or (not from_file and len(args) != 2):
  119. parser.print_help()
  120. return 1
  121. if not opts.force_unsafe_run:
  122. out, err = subprocess.Popen([_git, 'status', '--porcelain'],
  123. stdout=subprocess.PIPE).communicate()
  124. if out:
  125. print('ERROR: This tool does not print any confirmation prompts,')
  126. print('so you should only run it with a clean staging area and cache')
  127. print('so that reverting a bad find/replace is as easy as running')
  128. print(' git checkout -- .')
  129. print('')
  130. print('To override this safeguard, pass the -f flag.')
  131. return 1
  132. global_file_globs = ['*.*']
  133. if opts.use_default_glob:
  134. global_file_globs = ['*.cc', '*.h', '*.m', '*.mm']
  135. elif opts.user_supplied_globs:
  136. global_file_globs = opts.user_supplied_globs
  137. # Construct list of search-replace tasks.
  138. search_replace_tasks = []
  139. if opts.input_filename == '':
  140. original = args[0]
  141. replacement = args[1]
  142. search_replace_tasks.append([original, replacement, global_file_globs])
  143. else:
  144. f = open(opts.input_filename)
  145. search_replace_tasks = eval("".join(f.readlines()))
  146. for task in search_replace_tasks:
  147. if len(task) == 2:
  148. task.append(global_file_globs)
  149. f.close()
  150. for (original, replacement, file_globs) in search_replace_tasks:
  151. print('File globs: %s' % file_globs)
  152. print('Original: %s' % original)
  153. print('Replacement: %s' % replacement)
  154. MultiFileFindReplace(original, replacement, file_globs)
  155. return 0
  156. if __name__ == '__main__':
  157. sys.exit(main())