Skip to content

Instantly share code, notes, and snippets.

@jonlabelle
Last active March 12, 2024 05:44
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save jonlabelle/dd8c3caa7808cbe4cfe0a47ee4881059 to your computer and use it in GitHub Desktop.
Save jonlabelle/dd8c3caa7808cbe4cfe0a47ee4881059 to your computer and use it in GitHub Desktop.
Replace CRLF (windows) line endings with LF (unix) line endings in files.
#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment