#!/usr/bin/env python """Replace line breaks, from one format to another.""" from __future__ import print_function import argparse import glob import os import sys import tempfile from stat import ST_ATIME, ST_MTIME UNIX_NEWLINE = '\n' WINDOWS_NEWLINE = '\r\n' MAC_NEWLINE = '\r' def _normalize_line_endings(lines, line_ending='unix'): r"""Normalize line endings to unix (\n), windows (\r\n) or mac (\r). :param lines: The lines to normalize. :param line_ending: The line ending format. Acceptable values are 'unix' (default), 'windows' and 'mac'. :return: Line endings normalized. """ lines = lines.replace(WINDOWS_NEWLINE, UNIX_NEWLINE).replace(MAC_NEWLINE, UNIX_NEWLINE) if line_ending == 'windows': lines = lines.replace(UNIX_NEWLINE, WINDOWS_NEWLINE) elif line_ending == 'mac': lines = lines.replace(UNIX_NEWLINE, MAC_NEWLINE) return lines def _copy_file_time(source_file, destination_file): """Copy one file's atime and mtime to another. :param source_file: Source file. :param destination_file: Destination file. """ file1, file2 = source_file, destination_file try: stat1 = os.stat(file1) except os.error: sys.stderr.write(file1 + ' : cannot stat\n') sys.exit(1) try: os.utime(file2, (stat1[ST_ATIME], stat1[ST_MTIME])) except os.error: sys.stderr.write(file2 + ' : cannot change time\n') sys.exit(2) def _create_temp_file(contents): """Create a temp file. :param contents: The temp file contents. :return: The absolute path of the created temp file. """ tf = tempfile.NamedTemporaryFile(mode='wb', suffix='txt', delete=False) tf.write(contents) tf.close() return tf.name def _delete_file_if_exists(filepath): """Delete the file if it exists. :param filepath: The file path. """ if os.path.exists(filepath): os.remove(filepath) def _read_file_data(filepath): """Read file data. :param filepath: The file path. :return: The file contents. """ data = open(filepath, 'rb').read() return data def _write_file_data(filepath, data): """Write file data. :param filepath: The file path. :param data: The data to write. """ f = open(filepath, 'wb') f.write(data) f.close() def main(): """Main.""" parser = argparse.ArgumentParser( prog='crlf', description='Replace CRLF (windows) line endings with LF (unix) ' 'line endings in files (and vice-versa.') parser.add_argument( '-q', '--quiet', help='surpress descriptive messages from output', action='store_true', default=False) parser.add_argument( '-n', '--dryrun', help='show changes, but do not modify files', action='store_true', default=False) parser.add_argument( '-w', '--windows', help='replace LF (unix) line endings with CRLF (windows) line endings', action='store_true', default=False) parser.add_argument( '-u', '--unix', help='replace CRLF (windows) line endings with LF (unix) ' 'line endings (default)', action='store_true', default=False) parser.add_argument( '-t', '--timestamps', help="maintains the modfified file's time stamps (atime and mtime)", action='store_true', default=False) parser.add_argument( 'files', nargs='+', help="a list of files or file glob patterns to process", default='.') if len(sys.argv) < 2: parser.print_help() sys.exit(2) args = parser.parse_args() if args.windows is True and args.unix is True: sys.stderr.write("Ambiguous options specified, 'unix' and 'windows'. " "Please choose one option, or the other.\n") sys.exit(2) files_to_process = [] for argfile in args.files: files_to_process.extend(glob.glob(argfile)) if len(files_to_process) <= 0: if args.quiet is False: sys.stderr.write('No files matched the specified pattern.\n') sys.exit(2) if args.dryrun is True and args.quiet is False: print('Dry run only... files will NOT be modifed.') for file_to_process in files_to_process: if os.path.isdir(file_to_process): if args.quiet is False: print("- '{0}' : is a directory (skip)".format(file_to_process)) continue if os.path.isfile(file_to_process): data = _read_file_data(file_to_process) if '\\0' in data: if args.quiet is False: print("- '{0}' : is a binary file (skip)".format(file_to_process)) continue if args.windows is True: newdata = _normalize_line_endings(data, line_ending='windows') else: newdata = _normalize_line_endings(data, line_ending='unix') if newdata != data: if args.quiet is False: if args.windows is True: if args.dryrun is True: print("+ '{0}' : LF would be replaced with CRLF".format(file_to_process)) else: print("+ '{0}' : replacing LF with CRLF".format(file_to_process)) else: if args.dryrun is True: print("+ '{0}' : CRLF would be replaced with LF".format(file_to_process)) else: print("+ '{0}' : replacing CRLF with LF".format(file_to_process)) tmp_file_path = "" if args.dryrun is False: try: if args.timestamps is True: # create a temp file with the orignal file # contents and copy the old file's atime a mtime tmp_file_path = _create_temp_file(data) _copy_file_time(file_to_process, tmp_file_path) # overwrite the current file with the modfified contents _write_file_data(file_to_process, newdata) if args.timestamps is True: # copy the original file's atime and mtime back to # the orignial file w/ the modified contents, # and delete the temp file. _copy_file_time(tmp_file_path, file_to_process) _delete_file_if_exists(tmp_file_path) except Exception as ex: sys.stderr.write('error : {0}\n'.format(str(ex))) sys.exit(1) else: if args.quiet is False: if args.windows is True: print("- '{0}' : line endings are already CRLF (windows)".format(file_to_process)) else: print("- '{0}' : line endings are already LF (unix)".format(file_to_process)) else: sys.stderr.write("- '{0}' : file not found\n".format(file_to_process)) sys.exit(1) if __name__ == '__main__': main()