blob: 0328e8658cc2c6dfb3351ba2684f93f6166468f1 [file] [log] [blame]
Anthony Sottile8f615292022-01-15 19:24:05 -05001from __future__ import annotations
2
Morgan Courbetfc8a5b22017-06-13 21:38:14 +02003import argparse
Anthony Sottilefbcd0962017-09-05 20:20:43 -07004import collections
Anthony Sottile030bfac2019-01-31 19:19:10 -08005from typing import Sequence
Morgan Courbetfc8a5b22017-06-13 21:38:14 +02006
7
Anthony Sottilefbcd0962017-09-05 20:20:43 -07008CRLF = b'\r\n'
9LF = b'\n'
10CR = b'\r'
11# Prefer LF to CRLF to CR, but detect CRLF before LF
12ALL_ENDINGS = (CR, CRLF, LF)
13FIX_TO_LINE_ENDING = {'cr': CR, 'crlf': CRLF, 'lf': LF}
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020014
15
Anthony Sottilef5c42a02020-02-05 11:10:42 -080016def _fix(filename: str, contents: bytes, ending: bytes) -> None:
Anthony Sottilefbcd0962017-09-05 20:20:43 -070017 new_contents = b''.join(
18 line.rstrip(b'\r\n') + ending for line in contents.splitlines(True)
19 )
20 with open(filename, 'wb') as f:
21 f.write(new_contents)
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020022
23
Anthony Sottilef5c42a02020-02-05 11:10:42 -080024def fix_filename(filename: str, fix: str) -> int:
Anthony Sottilefbcd0962017-09-05 20:20:43 -070025 with open(filename, 'rb') as f:
26 contents = f.read()
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020027
Anthony Sottile8f615292022-01-15 19:24:05 -050028 counts: dict[bytes, int] = collections.defaultdict(int)
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020029
Anthony Sottilefbcd0962017-09-05 20:20:43 -070030 for line in contents.splitlines(True):
31 for ending in ALL_ENDINGS:
32 if line.endswith(ending):
33 counts[ending] += 1
34 break
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020035
Anthony Sottilefbcd0962017-09-05 20:20:43 -070036 # Some amount of mixed line endings
37 mixed = sum(bool(x) for x in counts.values()) > 1
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020038
Andy Gimblett59ed5122018-11-26 17:29:49 +000039 if fix == 'no' or (fix == 'auto' and not mixed):
Anthony Sottilefbcd0962017-09-05 20:20:43 -070040 return mixed
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020041
Anthony Sottilefbcd0962017-09-05 20:20:43 -070042 if fix == 'auto':
43 max_ending = LF
44 max_lines = 0
45 # ordering is important here such that lf > crlf > cr
46 for ending_type in ALL_ENDINGS:
47 # also important, using >= to find a max that prefers the last
48 if counts[ending_type] >= max_lines:
49 max_ending = ending_type
50 max_lines = counts[ending_type]
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020051
Anthony Sottilefbcd0962017-09-05 20:20:43 -070052 _fix(filename, contents, max_ending)
53 return 1
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020054 else:
Anthony Sottilefbcd0962017-09-05 20:20:43 -070055 target_ending = FIX_TO_LINE_ENDING[fix]
56 # find if there are lines with *other* endings
Anthony Sottile76047f62017-09-27 07:47:24 -070057 # It's possible there's no line endings of the target type
58 counts.pop(target_ending, None)
Anthony Sottilefbcd0962017-09-05 20:20:43 -070059 other_endings = bool(sum(counts.values()))
60 if other_endings:
61 _fix(filename, contents, target_ending)
62 return other_endings
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020063
64
Anthony Sottile8f615292022-01-15 19:24:05 -050065def main(argv: Sequence[str] | None = None) -> int:
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020066 parser = argparse.ArgumentParser()
67 parser.add_argument(
Anthony Sottilefbcd0962017-09-05 20:20:43 -070068 '-f', '--fix',
69 choices=('auto', 'no') + tuple(FIX_TO_LINE_ENDING),
70 default='auto',
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020071 help='Replace line ending with the specified. Default is "auto"',
72 )
73 parser.add_argument('filenames', nargs='*', help='Filenames to fix')
74 args = parser.parse_args(argv)
75
Anthony Sottilefbcd0962017-09-05 20:20:43 -070076 retv = 0
77 for filename in args.filenames:
Andy Gimblett59ed5122018-11-26 17:29:49 +000078 if fix_filename(filename, args.fix):
79 if args.fix == 'no':
Anthony Sottilef5c42a02020-02-05 11:10:42 -080080 print(f'{filename}: mixed line endings')
Andy Gimblett59ed5122018-11-26 17:29:49 +000081 else:
Anthony Sottilef5c42a02020-02-05 11:10:42 -080082 print(f'{filename}: fixed mixed line endings')
Andy Gimblett59ed5122018-11-26 17:29:49 +000083 retv = 1
Anthony Sottilefbcd0962017-09-05 20:20:43 -070084 return retv
Morgan Courbetfc8a5b22017-06-13 21:38:14 +020085
86
87if __name__ == '__main__':
Anthony Sottile39ab2ed2021-10-23 13:23:50 -040088 raise SystemExit(main())