summaryrefslogtreecommitdiffstats
path: root/pre_commit_hooks/check_executables_have_shebangs.py
blob: 34af5cac9f7a0f96d3e83dca4092d3ae4d62aaca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"""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:
    if sys.platform == 'win32':  # pragma: win32 cover
        return _check_git_filemode(paths)
    else:  # pragma: win32 no cover
        retv = 0
        for path in paths:
            if not has_shebang(path):
                _message(path)
                retv = 1

        return retv


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 on Windows, you may also need to: '
        f'`git add --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__':
    raise SystemExit(main())