mirror of
https://github.com/pre-commit/pre-commit-hooks.git
synced 2026-04-05 11:36:54 +00:00
check-executables-have-shebangs needs to check the actual file mode known to Git and avoid relying only on the filesystem permissions, because some filesystems force the executable bit on every file (e.g. fat32 on Linux).
76 lines
2.2 KiB
Python
76 lines
2.2 KiB
Python
"""Check that executable text files have a shebang."""
|
|
import argparse
|
|
import shlex
|
|
import sys
|
|
from typing import Generator
|
|
from typing import List
|
|
from typing import NamedTuple
|
|
from typing import Optional
|
|
from typing import Sequence
|
|
from typing import Set
|
|
|
|
from pre_commit_hooks.util import cmd_output
|
|
from pre_commit_hooks.util import zsplit
|
|
|
|
EXECUTABLE_VALUES = frozenset(('1', '3', '5', '7'))
|
|
|
|
|
|
def check_executables(paths: List[str]) -> int:
|
|
# Even if this hook is configured to be called only on files flagged
|
|
# executable by identify, we must check the real filemode known to Git
|
|
# because some filesystems force the executable bit (e.g. fat32 under
|
|
# Linux).
|
|
return _check_git_filemode(paths)
|
|
|
|
|
|
class GitLsFile(NamedTuple):
|
|
mode: str
|
|
filename: str
|
|
|
|
|
|
def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile, None, None]:
|
|
outs = cmd_output('git', 'ls-files', '-z', '--stage', '--', *paths)
|
|
for out in zsplit(outs):
|
|
metadata, filename = out.split('\t')
|
|
mode, _, _ = metadata.split()
|
|
yield GitLsFile(mode, filename)
|
|
|
|
|
|
def _check_git_filemode(paths: Sequence[str]) -> int:
|
|
seen: Set[str] = set()
|
|
for ls_file in git_ls_files(paths):
|
|
is_executable = any(b in EXECUTABLE_VALUES for b in ls_file.mode[-3:])
|
|
if is_executable and not has_shebang(ls_file.filename):
|
|
_message(ls_file.filename)
|
|
seen.add(ls_file.filename)
|
|
|
|
return int(bool(seen))
|
|
|
|
|
|
def has_shebang(path: str) -> int:
|
|
with open(path, 'rb') as f:
|
|
first_bytes = f.read(2)
|
|
|
|
return first_bytes == b'#!'
|
|
|
|
|
|
def _message(path: str) -> None:
|
|
print(
|
|
f'{path}: marked executable but has no (or invalid) shebang!\n'
|
|
f" If it isn't supposed to be executable, try: "
|
|
f'`chmod -x {shlex.quote(path)}`\n'
|
|
f' If it is supposed to be executable, double-check its shebang.',
|
|
file=sys.stderr,
|
|
)
|
|
|
|
|
|
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('filenames', nargs='*')
|
|
args = parser.parse_args(argv)
|
|
|
|
return check_executables(args.filenames)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
exit(main())
|