Add a checker for executables without shebangs
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index dac48f0..1aba741 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml
@@ -51,6 +51,15 @@ # for backward compatibility files: '' minimum_pre_commit_version: 0.15.0 +- id: check-executables-have-shebangs + name: Check that executables have shebangs + description: Ensures that (non-binary) executables have a shebang. + entry: check-executables-have-shebangs + language: python + types: [text, executable] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-json name: Check JSON description: This hook checks json files for parseable syntax.
diff --git a/README.md b/README.md index 6e268ff..dea0f5e 100644 --- a/README.md +++ b/README.md
@@ -34,6 +34,8 @@ case-insensitive filesystem like MacOS HFS+ or Windows FAT. - `check-docstring-first` - Checks for a common error of placing code before the docstring. +- `check-executables-have-shebangs` - Checks that non-binary executables have a + proper shebang. - `check-json` - Attempts to load all json files to verify syntax. - `check-merge-conflict` - Check for files that contain merge conflict strings. - `check-symlinks` - Checks for symlinks which do not point to anything.
diff --git a/hooks.yaml b/hooks.yaml index 7a4f933..5278bf5 100644 --- a/hooks.yaml +++ b/hooks.yaml
@@ -34,6 +34,12 @@ entry: upgrade-your-pre-commit-version files: '' minimum_pre_commit_version: 0.15.0 +- id: check-executables-have-shebangs + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: check-json language: system name: upgrade-your-pre-commit-version
diff --git a/pre_commit_hooks/check_executables_have_shebangs.py b/pre_commit_hooks/check_executables_have_shebangs.py new file mode 100644 index 0000000..89ac6e5 --- /dev/null +++ b/pre_commit_hooks/check_executables_have_shebangs.py
@@ -0,0 +1,40 @@ +"""Check that executable text files have a shebang.""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import pipes +import sys + + +def check_has_shebang(path): + with open(path, 'rb') as f: + first_bytes = f.read(2) + + if first_bytes != b'#!': + print( + '{path}: marked executable but has no (or invalid) shebang!\n' + " If it isn't supposed to be executable, try: chmod -x {quoted}\n" + ' If it is supposed to be executable, double-check its shebang.'.format( + path=path, + quoted=pipes.quote(path), + ), + file=sys.stderr, + ) + return 1 + else: + return 0 + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('filenames', nargs='*') + args = parser.parse_args(argv) + + retv = 0 + + for filename in args.filenames: + retv |= check_has_shebang(filename) + + return retv
diff --git a/setup.py b/setup.py index c5cceb7..2d99e5d 100644 --- a/setup.py +++ b/setup.py
@@ -39,6 +39,7 @@ 'check-byte-order-marker = pre_commit_hooks.check_byte_order_marker:main', 'check-case-conflict = pre_commit_hooks.check_case_conflict:main', 'check-docstring-first = pre_commit_hooks.check_docstring_first:main', + 'check-executables-have-shebangs = pre_commit_hooks.check_executables_have_shebangs:main', 'check-json = pre_commit_hooks.check_json:check_json', 'check-merge-conflict = pre_commit_hooks.check_merge_conflict:detect_merge_conflict', 'check-symlinks = pre_commit_hooks.check_symlinks:check_symlinks',
diff --git a/tests/check_executables_have_shebangs_test.py b/tests/check_executables_have_shebangs_test.py new file mode 100644 index 0000000..0e28986 --- /dev/null +++ b/tests/check_executables_have_shebangs_test.py
@@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +from pre_commit_hooks.check_executables_have_shebangs import main + + +@pytest.mark.parametrize('content', ( + b'#!/bin/bash\nhello world\n', + b'#!/usr/bin/env python3.6', + b'#!python', + '#!☃'.encode('UTF-8'), +)) +def test_has_shebang(content, tmpdir): + path = tmpdir.join('path') + path.write(content, 'wb') + assert main((path.strpath,)) == 0 + + +@pytest.mark.parametrize('content', ( + b'', + b' #!python\n', + b'\n#!python\n', + b'python\n', + '☃'.encode('UTF-8'), + +)) +def test_bad_shebang(content, tmpdir, capsys): + path = tmpdir.join('path') + path.write(content, 'wb') + assert main((path.strpath,)) == 1 + _, stderr = capsys.readouterr() + assert stderr.startswith('{}: marked executable but'.format(path.strpath))