1#!/usr/bin/env python
2# Copyright (c) 2011 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"""Find and fix files with inconsistent line endings.
7
8This script requires 'dos2unix.exe' and 'unix2dos.exe' from Cygwin; they
9must be in the user's PATH.
10
11Arg: Either one or more files to examine, or (with --file-list) one or more
12    files that themselves contain lists of files. The argument(s) passed to
13    this script, as well as the paths in the file if any, may be relative or
14    absolute Windows-style paths (with either type of slash). The list might
15    be generated with 'find -type f' or extracted from a gcl change listing,
16    for example.
17"""
18
19import errno
20import logging
21import optparse
22import subprocess
23import sys
24
25
26# Whether to produce excessive debugging output for each file in the list.
27DEBUGGING = False
28
29
30class Error(Exception):
31  """Local exception class."""
32  pass
33
34
35def CountChars(text, str):
36  """Count the number of instances of the given string in the text."""
37  split = text.split(str)
38  logging.debug(len(split) - 1)
39  return len(split) - 1
40
41
42def PrevailingEOLName(crlf, cr, lf):
43  """Describe the most common line ending.
44
45  Args:
46    crlf: How many CRLF (\r\n) sequences are in the file.
47    cr: How many CR (\r) characters are in the file, excluding CRLF sequences.
48    lf: How many LF (\n) characters are in the file, excluding CRLF sequences.
49
50  Returns:
51    A string describing the most common of the three line endings.
52  """
53  most = max(crlf, cr, lf)
54  if most == cr:
55    return 'cr'
56  if most == crlf:
57    return 'crlf'
58  return 'lf'
59
60
61def FixEndings(file, crlf, cr, lf):
62  """Change the file's line endings to CRLF or LF, whichever is more common."""
63  most = max(crlf, cr, lf)
64  if most == crlf:
65    result = subprocess.call('unix2dos.exe %s' % file, shell=True)
66    if result:
67      raise Error('Error running unix2dos.exe %s' % file)
68  else:
69    result = subprocess.call('dos2unix.exe %s' % file, shell=True)
70    if result:
71      raise Error('Error running dos2unix.exe %s' % file)
72
73
74def ProcessFiles(filelist):
75  """Fix line endings in each file in the filelist list."""
76  for filename in filelist:
77    filename = filename.strip()
78    logging.debug(filename)
79    try:
80      # Open in binary mode to preserve existing line endings.
81      text = open(filename, 'rb').read()
82    except IOError, e:
83      if e.errno != errno.ENOENT:
84        raise
85      logging.warning('File %s not found.' % filename)
86      continue
87
88    crlf = CountChars(text, '\r\n')
89    cr = CountChars(text, '\r') - crlf
90    lf = CountChars(text, '\n') - crlf
91
92    if options.force_lf:
93      if crlf > 0 or cr > 0:
94        print '%s: forcing to LF' % filename
95        # Fudge the counts to force switching to LF.
96        FixEndings(filename, 0, 0, 1)
97    else:
98      if ((crlf > 0 and cr > 0) or
99          (crlf > 0 and lf > 0) or
100          (  lf > 0 and cr > 0)):
101        print '%s: mostly %s' % (filename, PrevailingEOLName(crlf, cr, lf))
102        FixEndings(filename, crlf, cr, lf)
103
104
105def process(options, args):
106  """Process the files."""
107  if not args or len(args) < 1:
108    raise Error('No files given.')
109
110  if options.file_list:
111    for arg in args:
112      filelist = open(arg, 'r').readlines()
113      ProcessFiles(filelist)
114  else:
115    filelist = args
116    ProcessFiles(filelist)
117  return 0
118
119
120def main():
121  if DEBUGGING:
122    debug_level = logging.DEBUG
123  else:
124    debug_level = logging.INFO
125  logging.basicConfig(level=debug_level,
126                      format='%(asctime)s %(levelname)-7s: %(message)s',
127                      datefmt='%H:%M:%S')
128
129  option_parser = optparse.OptionParser()
130  option_parser.add_option("", "--file-list", action="store_true",
131                           default=False,
132                           help="Treat the arguments as files containing "
133                                "lists of files to examine, rather than as "
134                                "the files to be checked.")
135  option_parser.add_option("", "--force-lf", action="store_true",
136                           default=False,
137                           help="Force any files with CRLF to LF instead.")
138  options, args = option_parser.parse_args()
139  return process(options, args)
140
141
142if '__main__' == __name__:
143  sys.exit(main())
144