asdf

Форк
0
/
checkstyle.py 
275 строк · 9.0 Кб
1
#!/usr/bin/env python3
2
import re
3
import os
4
import argparse
5
from pathlib import Path
6
from typing import Callable, List, Dict, Any # compat
7

8
# This file checks Bash and Shell scripts for violations not found with
9
# shellcheck or existing methods. You can use it in several ways:
10
#
11
# Lint all .bash, .sh, .bats files along with 'bin/asdf' and print out violations:
12
# $ ./scripts/checkstyle.py
13
#
14
# The former, but also fix all violations. This must be ran until there
15
# are zero violations since any line can have more than one violation:
16
# $ ./scripts/checkstyle.py --fix
17
#
18
# Lint a particular file:
19
# $ ./scripts/checkstyle.py ./lib/functions/installs.bash
20
#
21
# Check to ensure all regular expressions are working as intended:
22
# $ ./scripts/checkstyle.py --internal-test-regex
23

24
Rule = Dict[str, Any]
25

26
class c:
27
    RED = '\033[91m'
28
    GREEN = '\033[92m'
29
    YELLOW = '\033[93m'
30
    BLUE = '\033[94m'
31
    MAGENTA = '\033[95m'
32
    CYAN = '\033[96m'
33
    RESET = '\033[0m'
34
    BOLD = '\033[1m'
35
    UNDERLINE = '\033[4m'
36
    LINK: Callable[[str, str], str] = lambda href, text: f'\033]8;;{href}\a{text}\033]8;;\a'
37

38
def utilGetStrs(line: Any, m: Any):
39
    return (
40
        line[0:m.start('match')],
41
        line[m.start('match'):m.end('match')],
42
        line[m.end('match'):]
43
    )
44

45
# Before: printf '%s\\n' '^w^'
46
# After: printf '%s\n' '^w^'
47
def noDoubleBackslashFixer(line: str, m: Any) -> str:
48
    prestr, midstr, poststr = utilGetStrs(line, m)
49

50
    return f'{prestr}{midstr[1:]}{poststr}'
51

52
# Before: $(pwd)
53
# After: $PWD
54
def noPwdCaptureFixer(line: str, m: Any) -> str:
55
    prestr, _, poststr = utilGetStrs(line, m)
56

57
    return f'{prestr}$PWD{poststr}'
58

59
# Before: [ a == b ]
60
# After: [ a = b ]
61
def noTestDoubleEqualsFixer(line: str, m: Any) -> str:
62
    prestr, _, poststr = utilGetStrs(line, m)
63

64
    return f'{prestr}={poststr}'
65

66
# Before: function fn() { ...
67
# After: fn() { ...
68
# ---
69
# Before: function fn { ...
70
# After fn() { ...
71
def noFunctionKeywordFixer(line: str, m: Any) -> str:
72
    prestr, midstr, poststr = utilGetStrs(line, m)
73

74
    midstr = midstr.strip()
75
    midstr = midstr[len('function'):]
76
    midstr = midstr.strip()
77

78
    parenIdx = midstr.find('(')
79
    if parenIdx != -1: midstr = midstr[:parenIdx]
80

81
    return f'{prestr}{midstr}() {poststr}'
82

83
# Before: >/dev/null 2>&1
84
# After: &>/dev/null
85
# ---
86
# Before: 2>/dev/null 1>&2
87
# After: &>/dev/null
88
def noVerboseRedirectionFixer(line: str, m: Any) -> str:
89
    prestr, _, poststr = utilGetStrs(line, m)
90

91
    return f'{prestr}&>/dev/null{poststr}'
92

93
def lintfile(file: Path, rules: List[Rule], options: Dict[str, Any]):
94
    content_arr = file.read_text().split('\n')
95

96
    for line_i, line in enumerate(content_arr):
97
        if 'checkstyle-ignore' in line:
98
            continue
99

100
        for rule in rules:
101
            should_run = False
102
            if 'sh' in rule['fileTypes']:
103
                if file.name.endswith('.sh') or str(file.absolute()).endswith('bin/asdf'):
104
                    should_run = True
105
            if 'bash' in rule['fileTypes']:
106
                if file.name.endswith('.bash') or file.name.endswith('.bats'):
107
                    should_run = True
108

109
            if options['verbose']:
110
                print(f'{str(file)}: {should_run}')
111

112
            if not should_run:
113
                continue
114

115
            m = re.search(rule['regex'], line)
116
            if m is not None and m.group('match') is not None:
117
                dir = os.path.relpath(file.resolve(), Path.cwd())
118
                prestr = line[0:m.start('match')]
119
                midstr = line[m.start('match'):m.end('match')]
120
                poststr = line[m.end('match'):]
121

122
                print(f'{c.CYAN}{dir}{c.RESET}:{line_i + 1}')
123
                print(f'{c.MAGENTA}{rule["name"]}{c.RESET}: {rule["reason"]}')
124
                print(f'{prestr}{c.RED}{midstr}{c.RESET}{poststr}')
125
                print()
126

127
                if options['fix']:
128
                    content_arr[line_i] = rule['fixerFn'](line, m)
129

130
                rule['found'] += 1
131

132
    if options['fix']:
133
        file.write_text('\n'.join(content_arr))
134

135
def main():
136
    rules: List[Rule] = [
137
        {
138
            'name': 'no-double-backslash',
139
            'regex': '".*?(?P<match>\\\\\\\\[abeEfnrtv\'"?xuUc]).*?(?<!\\\\)"',
140
            'reason': 'Backslashes are only required if followed by a $, `, ", \\, or <newline>',
141
            'fileTypes': ['bash', 'sh'],
142
            'fixerFn': noDoubleBackslashFixer,
143
            'testPositiveMatches': [
144
                'printf "%s\\\\n" "Hai"',
145
                'echo -n "Hello\\\\n"'
146
            ],
147
            'testNegativeMatches': [
148
                'printf "%s\\n" "Hai"',
149
                'echo -n "Hello\\n"'
150
            ],
151
        },
152
        {
153
            'name': 'no-pwd-capture',
154
            'regex': '(?P<match>\\$\\(pwd\\))',
155
            'reason': '$PWD is essentially equivalent to $(pwd) without the overhead of a subshell',
156
            'fileTypes': ['bash', 'sh'],
157
            'fixerFn': noPwdCaptureFixer,
158
            'testPositiveMatches': [
159
                '$(pwd)'
160
            ],
161
            'testNegativeMatches': [
162
                '$PWD'
163
            ],
164
        },
165
        {
166
            'name': 'no-test-double-equals',
167
            'regex': '(?<!\\[)\\[ (?:[^]]|](?=}))*?(?P<match>==).*?]',
168
            'reason': 'Disallow double equals in places where they are not necessary for consistency',
169
            'fileTypes': ['bash', 'sh'],
170
            'fixerFn': noTestDoubleEqualsFixer,
171
            'testPositiveMatches': [
172
                '[ a == b ]',
173
                '[ "${lines[0]}" == blah ]',
174
            ],
175
            'testNegativeMatches': [
176
                '[ a = b ]',
177
                '[[ a = b ]]',
178
                '[[ a == b ]]',
179
                '[ a = b ] || [[ a == b ]]',
180
                '[[ a = b ]] || [[ a == b ]]',
181
                '[[ "${lines[0]}" == \'usage: \'* ]]',
182
                '[ "${lines[0]}" = blah ]',
183
            ],
184
        },
185
        {
186
            'name': 'no-function-keyword',
187
            'regex': '^[ \\t]*(?P<match>function .*?(?:\\([ \\t]*\\))?[ \\t]*){',
188
            'reason': 'Only allow functions declared like `fn_name() {{ :; }}` for consistency (see ' + c.LINK('https://www.shellcheck.net/wiki/SC2113', 'ShellCheck SC2113') + ')',
189
            'fileTypes': ['bash', 'sh'],
190
            'fixerFn': noFunctionKeywordFixer,
191
            'testPositiveMatches': [
192
                'function fn() { :; }',
193
                'function fn { :; }',
194
            ],
195
            'testNegativeMatches': [
196
                'fn() { :; }',
197
            ],
198
        },
199
        {
200
            'name': 'no-verbose-redirection',
201
            'regex': '(?P<match>(>/dev/null 2>&1|2>/dev/null 1>&2))',
202
            'reason': 'Use `&>/dev/null` instead of `>/dev/null 2>&1` or `2>/dev/null 1>&2` for consistency',
203
            'fileTypes': ['bash'],
204
            'fixerFn': noVerboseRedirectionFixer,
205
            'testPositiveMatches': [
206
                'echo woof >/dev/null 2>&1',
207
                'echo woof 2>/dev/null 1>&2',
208
            ],
209
            'testNegativeMatches': [
210
                'echo woof &>/dev/null',
211
                'echo woof >&/dev/null',
212
            ],
213
        },
214
    ]
215
    [rule.update({ 'found': 0 }) for rule in rules]
216

217
    parser = argparse.ArgumentParser()
218
    parser.add_argument('files', metavar='FILES', nargs='*')
219
    parser.add_argument('--fix', action='store_true')
220
    parser.add_argument('--verbose', action='store_true')
221
    parser.add_argument('--internal-test-regex', action='store_true')
222
    args = parser.parse_args()
223

224
    if args.internal_test_regex:
225
        for rule in rules:
226
            for positiveMatch in rule['testPositiveMatches']:
227
                m: Any = re.search(rule['regex'], positiveMatch)
228
                if m is None or m.group('match') is None:
229
                    print(f'{c.MAGENTA}{rule["name"]}{c.RESET}: Failed {c.CYAN}positive{c.RESET} test:')
230
                    print(f'=> {positiveMatch}')
231
                    print()
232

233
            for negativeMatch in rule['testNegativeMatches']:
234
                m: Any = re.search(rule['regex'], negativeMatch)
235
                if m is not None and m.group('match') is not None:
236
                    print(f'{c.MAGENTA}{rule["name"]}{c.RESET}: Failed {c.YELLOW}negative{c.RESET} test:')
237
                    print(f'=> {negativeMatch}')
238
                    print()
239
        print('Done.')
240
        return
241

242
    options = {
243
        'fix': args.fix,
244
        'verbose': args.verbose,
245
    }
246

247
    # parse files and print matched lints
248
    if len(args.files) > 0:
249
        for file in args.files:
250
            p = Path(file)
251
            if p.is_file():
252
                lintfile(p, rules, options)
253
    else:
254
        for file in Path.cwd().glob('**/*'):
255
            if '.git' in str(file.absolute()):
256
                continue
257

258
            if file.is_file():
259
                lintfile(file, rules, options)
260

261
    # print final results
262
    print(f'{c.UNDERLINE}TOTAL ISSUES{c.RESET}')
263
    for rule in rules:
264
        print(f'{c.MAGENTA}{rule["name"]}{c.RESET}: {rule["found"]}')
265

266
    grand_total = sum([rule['found'] for rule in rules])
267
    print(f'GRAND TOTAL: {grand_total}')
268

269
    # exit
270
    if grand_total == 0:
271
        exit(0)
272
    else:
273
        exit(2)
274

275
main()
276

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.