Check in a quick tool for sorting the #includes in a file.

This is especially useful when you do a mass-rename of a header
and now need to fix every user of it.

Special thanks to pamg for fixing my Python style!

Review URL: http://codereview.chromium.org/3149032

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@57435 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/tools/sort-headers.py b/tools/sort-headers.py
new file mode 100755
index 0000000..b18ac6c
--- /dev/null
+++ b/tools/sort-headers.py
@@ -0,0 +1,94 @@
+#!/usr/bin/python
+
+"""Given a filename as an argument, sort the #include/#imports in that file.
+
+Shows a diff and prompts for confirmation before doing the deed.
+"""
+
+import optparse
+import os
+import sys
+import termios
+import tty
+
+def YesNo(prompt):
+  """Prompts with a yes/no question, returns True if yes."""
+  print prompt,
+  sys.stdout.flush()
+  # http://code.activestate.com/recipes/134892/
+  fd = sys.stdin.fileno()
+  old_settings = termios.tcgetattr(fd)
+  ch = 'n'
+  try:
+    tty.setraw(sys.stdin.fileno())
+    ch = sys.stdin.read(1)
+  finally:
+    termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+  print ch
+  return ch in ('Y', 'y')
+
+
+def IncludeCompareKey(line):
+  """Sorting comparator key used for comparing two #include lines.
+  Returns the filename without the #include/#import prefix.
+  """
+  for prefix in ('#include ', '#import '):
+    if line.startswith(prefix):
+      return line[len(prefix):]
+  return line
+
+
+def IsInclude(line):
+  """Returns True if the line is an #include/#import line."""
+  return line.startswith('#include ') or line.startswith('#import ')
+
+
+def SortHeader(infile, outfile):
+  """Sorts the headers in infile, writing the sorted file to outfile."""
+  for line in infile:
+    if IsInclude(line):
+      headerblock = []
+      while IsInclude(line):
+        headerblock.append(line)
+        line = infile.next()
+      for header in sorted(headerblock, key=IncludeCompareKey):
+        outfile.write(header)
+      # Intentionally fall through, to write the line that caused
+      # the above while loop to exit.
+    outfile.write(line)
+
+
+def main():
+  parser = optparse.OptionParser(usage='%prog filename1 filename2 ...')
+  opts, args = parser.parse_args()
+
+  if len(args) < 1:
+    parser.print_help()
+    sys.exit(1)
+
+  for filename in args:
+    fixfilename = filename + '.new'
+    infile = open(filename, 'r')
+    outfile = open(fixfilename, 'w')
+    SortHeader(infile, outfile)
+    infile.close()
+    outfile.close()  # Important so the below diff gets the updated contents.
+
+    try:
+      diff = os.system('diff -u %s %s' % (filename, fixfilename))
+      if diff >> 8 == 0:  # Check exit code.
+        print '%s: no change' % filename
+        continue
+
+      if YesNo('Use new file (y/N)?'):
+        os.rename(fixfilename, filename)
+    finally:
+      try:
+        os.remove(fixfilename)
+      except OSError:
+        # If the file isn't there, we don't care.
+        pass
+
+
+if __name__ == '__main__':
+  main()