Skip to main content

Replace Unix line endings with Windows line endings, and vice-versa in Python.

#!/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

LF = '\n'
CRLF = '\r\n'
CR = '\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(CRLF, LF).replace(CR, LF)
    if line_ending == 'windows':
        lines = lines.replace(LF, CRLF)
    elif line_ending == 'mac':
        lines = lines.replace(LF, CR)
    return lines


def _copy_file_time(source, destination):
    """Copy one file's atime and mtime to another.

    :param source: Source file.
    :param destination: Destination file.
    """
    file1, file2 = source, destination

    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='suppress 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 modified 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 arg_file in args.files:
        files_to_process.extend(glob.glob(arg_file))

    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 modified.')

    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:
                new_data = _normalize_line_endings(data, line_ending='windows')
            else:
                new_data = _normalize_line_endings(data, line_ending='unix')

            if new_data != 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 original 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 modified contents
                        _write_file_data(file_to_process, new_data)

                        if args.timestamps is True:
                            # copy the original file's atime and mtime back to
                            # the original 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 already CRLF (windows)".format(file_to_process))
                    else:
                        print("- '{0}' : line endings 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()