1import argparse 2import collections 3from typing import Dict 4from typing import Optional 5from typing import Sequence 6 7 8CRLF = 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} 14 15 16def _fix(filename: str, contents: bytes, ending: bytes) -> None: 17 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) 22 23 24def fix_filename(filename: str, fix: str) -> int: 25 with open(filename, 'rb') as f: 26 contents = f.read() 27 28 counts: Dict[bytes, int] = collections.defaultdict(int) 29 30 for line in contents.splitlines(True): 31 for ending in ALL_ENDINGS: 32 if line.endswith(ending): 33 counts[ending] += 1 34 break 35 36 # Some amount of mixed line endings 37 mixed = sum(bool(x) for x in counts.values()) > 1 38 39 if fix == 'no' or (fix == 'auto' and not mixed): 40 return mixed 41 42 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] 51 52 _fix(filename, contents, max_ending) 53 return 1 54 else: 55 target_ending = FIX_TO_LINE_ENDING[fix] 56 # find if there are lines with *other* endings 57 # It's possible there's no line endings of the target type 58 counts.pop(target_ending, None) 59 other_endings = bool(sum(counts.values())) 60 if other_endings: 61 _fix(filename, contents, target_ending) 62 return other_endings 63 64 65def main(argv: Optional[Sequence[str]] = None) -> int: 66 parser = argparse.ArgumentParser() 67 parser.add_argument( 68 '-f', '--fix', 69 choices=('auto', 'no') + tuple(FIX_TO_LINE_ENDING), 70 default='auto', 71 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 76 retv = 0 77 for filename in args.filenames: 78 if fix_filename(filename, args.fix): 79 if args.fix == 'no': 80 print(f'{filename}: mixed line endings') 81 else: 82 print(f'{filename}: fixed mixed line endings') 83 retv = 1 84 return retv 85 86 87if __name__ == '__main__': 88 exit(main()) 89