summaryrefslogtreecommitdiffstats
path: root/test/lib/ansible_test/_util/controller/sanity/code-smell/shebang.py
blob: b0b13197839a2efee12f8b4cb353a7b690fd52b8 (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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Check shebangs, execute bits and byte order marks."""
from __future__ import annotations

import os
import re
import stat
import sys


def main():
    """Main entry point."""
    standard_shebangs = set([
        b'#!/bin/bash -eu',
        b'#!/bin/bash -eux',
        b'#!/bin/sh',
        b'#!/usr/bin/env bash',
        b'#!/usr/bin/env fish',
        b'#!/usr/bin/env pwsh',
        b'#!/usr/bin/env python',
        b'#!/usr/bin/make -f',
    ])

    integration_shebangs = set([
        b'#!/bin/sh',
        b'#!/usr/bin/env bash',
        b'#!/usr/bin/env python',
    ])

    module_shebangs = {
        '': b'#!/usr/bin/python',
        '.py': b'#!/usr/bin/python',
        '.ps1': b'#!powershell',
    }

    # see https://unicode.org/faq/utf_bom.html#bom1
    byte_order_marks = (
        (b'\x00\x00\xFE\xFF', 'UTF-32 (BE)'),
        (b'\xFF\xFE\x00\x00', 'UTF-32 (LE)'),
        (b'\xFE\xFF', 'UTF-16 (BE)'),
        (b'\xFF\xFE', 'UTF-16 (LE)'),
        (b'\xEF\xBB\xBF', 'UTF-8'),
    )

    for path in sys.argv[1:] or sys.stdin.read().splitlines():
        with open(path, 'rb') as path_fd:
            shebang = path_fd.readline().strip()
            mode = os.stat(path).st_mode
            executable = (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) & mode

            if not shebang or not shebang.startswith(b'#!'):
                if executable:
                    print('%s:%d:%d: file without shebang should not be executable' % (path, 0, 0))

                for mark, name in byte_order_marks:
                    if shebang.startswith(mark):
                        print('%s:%d:%d: file starts with a %s byte order mark' % (path, 0, 0, name))
                        break

                continue

            is_module = False
            is_integration = False

            dirname = os.path.dirname(path)

            if path.startswith('lib/ansible/modules/'):
                is_module = True
            elif re.search('^test/support/[^/]+/plugins/modules/', path):
                is_module = True
            elif re.search('^test/support/[^/]+/collections/ansible_collections/[^/]+/[^/]+/plugins/modules/', path):
                is_module = True
            elif path == 'test/lib/ansible_test/_util/target/cli/ansible_test_cli_stub.py':
                pass  # ansible-test entry point must be executable and have a shebang
            elif re.search(r'^lib/ansible/cli/[^/]+\.py', path):
                pass  # cli entry points must be executable and have a shebang
            elif path.startswith('examples/'):
                continue  # examples trigger some false positives due to location
            elif path.startswith('lib/') or path.startswith('test/lib/'):
                if executable:
                    print('%s:%d:%d: should not be executable' % (path, 0, 0))

                if shebang:
                    print('%s:%d:%d: should not have a shebang' % (path, 0, 0))

                continue
            elif path.startswith('test/integration/targets/') or path.startswith('tests/integration/targets/'):
                is_integration = True

                if dirname.endswith('/library') or '/plugins/modules' in dirname or dirname in (
                        # non-standard module library directories
                        'test/integration/targets/module_precedence/lib_no_extension',
                        'test/integration/targets/module_precedence/lib_with_extension',
                ):
                    is_module = True
            elif path.startswith('plugins/modules/'):
                is_module = True

            if is_module:
                if executable:
                    print('%s:%d:%d: module should not be executable' % (path, 0, 0))

                ext = os.path.splitext(path)[1]
                expected_shebang = module_shebangs.get(ext)
                expected_ext = ' or '.join(['"%s"' % k for k in module_shebangs])

                if expected_shebang:
                    if shebang == expected_shebang:
                        continue

                    print('%s:%d:%d: expected module shebang "%s" but found: %s' % (path, 1, 1, expected_shebang, shebang))
                else:
                    print('%s:%d:%d: expected module extension %s but found: %s' % (path, 0, 0, expected_ext, ext))
            else:
                if is_integration:
                    allowed = integration_shebangs
                else:
                    allowed = standard_shebangs

                if shebang not in allowed:
                    print('%s:%d:%d: unexpected non-module shebang: %s' % (path, 1, 1, shebang))


if __name__ == '__main__':
    main()