diff --git a/manifest.yaml b/manifest.yaml index 00b48d9..8686643 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -19,3 +19,8 @@ description: This verifies that test files are named correctly entry: name-tests-test language: python +- id: end-of-file-fixer + name: Fix End of Files + description: Ensures that a file is either empty, or ends with one newline. + entry: end-of-file-fixer + language: python diff --git a/pre_commit_hooks/end_of_file_fixer.py b/pre_commit_hooks/end_of_file_fixer.py new file mode 100644 index 0000000..b585ce3 --- /dev/null +++ b/pre_commit_hooks/end_of_file_fixer.py @@ -0,0 +1,70 @@ + +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import os +import sys + +from pre_commit_hooks.util import entry + + +def fix_file(file_obj): + # Test for newline at end of file + # Empty files will throw IOError here + try: + file_obj.seek(-1, os.SEEK_END) + except IOError: + return 0 + last_character = file_obj.read(1) + # last_character will be '' for an empty file + if last_character != '\n' and last_character != '': + file_obj.write('\n') + return 1 + + while last_character == '\n': + # Deal with the beginning of the file + if file_obj.tell() == 1: + # If we've reached the beginning of the file and it is all + # linebreaks then we can make this file empty + file_obj.seek(0) + file_obj.truncate() + return 1 + + # Go back two bytes and read a character + file_obj.seek(-2, os.SEEK_CUR) + last_character = file_obj.read(1) + + # Our current position is at the end of the file just before any amount of + # newlines. If we read two characters and get two newlines back we know + # there are extraneous newlines at the ned of the file. Then backtrack and + # trim the end off. + if len(file_obj.read(2)) == 2: + file_obj.seek(-1, os.SEEK_CUR) + file_obj.truncate() + return 1 + + return 0 + + +@entry +def end_of_file_fixer(argv): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', help='Filenames to fix') + args = parser.parse_args(argv) + + retv = 0 + + for filename in args.filenames: + # Read as binary so we can read byte-by-byte + with open(filename, 'rb+') as file_obj: + ret_for_file = fix_file(file_obj) + if ret_for_file: + print('Fixing {0}'.format(filename)) + retv |= ret_for_file + + return retv + + +if __name__ == '__main__': + sys.exit(end_of_file_fixer()) diff --git a/setup.py b/setup.py index 42afa6a..96d8d3a 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ setup( 'debug-statement-hook = pre_commit_hooks.debug_statement_hook:debug_statement_hook', 'trailing-whitespace-fixer = pre_commit_hooks.trailing_whitespace_fixer:fix_trailing_whitespace', 'name-tests-test = pre_commit_hooks.tests_should_end_in_test:validate_files', + 'end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:end_of_file_fixer', ], }, ) diff --git a/tests/end_of_file_fixer_test.py b/tests/end_of_file_fixer_test.py new file mode 100644 index 0000000..444836e --- /dev/null +++ b/tests/end_of_file_fixer_test.py @@ -0,0 +1,42 @@ + +import cStringIO +import os.path +import pytest + +from pre_commit_hooks.end_of_file_fixer import end_of_file_fixer +from pre_commit_hooks.end_of_file_fixer import fix_file + + +# Input, expected return value, expected output +TESTS = ( + ('foo\n', 0, 'foo\n'), + ('', 0, ''), + ('\n\n', 1, ''), + ('\n\n\n\n', 1, ''), + ('foo', 1, 'foo\n'), + ('foo\n\n\n', 1, 'foo\n'), + ('\xe2\x98\x83', 1, '\xe2\x98\x83\n'), +) + + +@pytest.mark.parametrize(('input', 'expected_retval', 'output'), TESTS) +def test_fix_file(input, expected_retval, output): + file_obj = cStringIO.StringIO() + file_obj.write(input) + ret = fix_file(file_obj) + assert file_obj.getvalue() == output + assert ret == expected_retval + + +@pytest.mark.parametrize(('input', 'expected_retval', 'output'), TESTS) +def test_integration(input, expected_retval, output, tmpdir): + file_path = os.path.join(tmpdir.strpath, 'file.txt') + + with open(file_path, 'w') as file_obj: + file_obj.write(input) + + ret = end_of_file_fixer([file_path]) + file_output = open(file_path, 'r').read() + + assert file_output == output + assert ret == expected_retval