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))