This file is indexed.

/usr/lib/python3/dist-packages/bashate/bashate.py is in python3-bashate 0.5.1-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
#!/usr/bin/env python
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from __future__ import absolute_import

import argparse
import fileinput
import os
import re
import subprocess
import sys

from bashate import messages

MESSAGES = messages.MESSAGES


def is_continuation(line):
    return re.search('\\\\\s*$', line)


def check_for_do(line, report):
    if not is_continuation(line):
        match = re.match('^\s*(for|while|until)\s', line)
        if match:
            operator = match.group(1).strip()
            if operator == "for":
                # "for i in ..." and "for ((" is bash, but
                # "for (" is likely from an embedded awk script,
                # so skip it
                if re.search('for \([^\(]', line):
                    return
            if not re.search(';\s*do$', line):
                report.print_error((MESSAGES['E010'].msg % operator), line)


def check_if_then(line, report):
    if not is_continuation(line):
        if re.search('^\s*(el)?if \[', line):
            if not re.search(';\s*then$', line):
                report.print_error(MESSAGES['E011'].msg, line)


def check_no_trailing_whitespace(line, report):
    if re.search('[ \t]+$', line):
        report.print_error(MESSAGES['E001'].msg, line)


def check_no_long_lines(line, report):
    if len(line.rstrip("\r\n")) > 79:
        report.print_error(MESSAGES['E006'].msg, line)


def check_indents(logical_line, report):
    # this is rather complex to handle argument offset indenting;
    # primarily done by emacs.  If there is an argument, it will try
    # to line up the following arguments underneath it, e.g.
    #   foobar_cmd bar baz \
    #              moo boo
    # Thus the offset in this case might not be a strict multiple of 4

    # Find the offset of the first argument of the command (if it has
    # one)
    m = re.search('^(?P<indent>[ \t]+)?(?P<cmd>\S+)(?P<ws>\s+)(?P<arg>\S+)',
                  logical_line[0])
    arg_offset = None
    if m:
        arg_offset = len(m.group('indent')) if m.group('indent') else 0
        arg_offset += len(m.group('cmd')) + len(m.group('ws'))

    # go through each line
    for lineno, line in enumerate(logical_line):
        m = re.search('^(?P<indent>[ \t]+)', line)
        if m:
            # no tabs, only spaces
            if re.search('\t', m.group('indent')):
                report.print_error(MESSAGES['E002'].msg, line)

            offset = len(m.group('indent'))

            # the first line and lines without an argument should be
            # offset by 4 spaces
            if (lineno == 0) or (arg_offset is None):
                if (offset % 4) != 0:
                    report.print_error(MESSAGES['E003'].msg, line)
            else:
                # other lines are allowed to line up with the first
                # argument, or be multiple-of 4 spaces
                if offset != arg_offset and (offset % 4) != 0:
                    report.print_error(MESSAGES['E003'].msg, line)


def check_function_decl(line, report):
    failed = False
    if line.startswith("function"):
        if not re.search('^function [\w-]* \{$', line):
            failed = True
    else:
        # catch the case without "function", e.g.
        # things like '^foo() {'
        if re.search('^\s*?\(\)\s*?\{', line):
            failed = True

    if failed:
        report.print_error(MESSAGES['E020'].msg, line)


def starts_heredoc(line):
    # note, watch out for <<EOF and <<'EOF' ; quotes in the
    # deliminator are part of syntax
    m = re.search("[^<]<<\s*([\'\"]?)(?P<token>\w+)([\'\"]?)", line)
    return m.group('token') if m else False


def end_of_heredoc(line, token):
    return token and re.search("^%s\s*$" % token, line)


def check_arithmetic(line, report):
    if "$[" in line:
        report.print_error(MESSAGES['E041'].msg, line)


def check_bare_arithmetic(line, report):
    if line.lstrip().startswith("(("):
        report.print_error(MESSAGES['E043'].msg, line)


def check_local_subshell(line, report):
    # XXX: should we increase the string checking to see if the $( is
    # anywhere with a string being set?  Risk of false positives?x
    if line.lstrip().startswith('local ') and \
       any(s in line for s in ('=$(', '=`', '="$(', '="`')):
        report.print_error(MESSAGES['E042'].msg, line)


def check_hashbang(line, filename, report):
    # this check only runs on the first line
    #  maybe this should check for shell?
    if not line.startswith("#!") and not filename.endswith(".sh"):
        report.print_error(MESSAGES['E005'].msg, line)


def check_syntax(filename, report):
    # run the file through "bash -n" to catch basic syntax errors and
    # other warnings
    matches = []

    # sample lines we want to match:
    # foo.sh: line 4: warning: \
    #    here-document at line 1 delimited by end-of-file (wanted `EOF')
    # foo.sh: line 9: syntax error: unexpected end of file
    # foo.sh: line 7: syntax error near unexpected token `}'
    #
    # i.e. consistency with ":"'s isn't constant, so just do our
    # best...
    r = re.compile(
        '^(?P<file>.*): line (?P<lineno>[0-9]+): (?P<error>.*)')
    # we are parsing the error message, so force it to ignore the
    # system locale so we don't get messages in another language
    bash_environment = os.environ
    bash_environment['LC_ALL'] = 'C'
    proc = subprocess.Popen(
        ['bash', '-n', filename], stdout=subprocess.PIPE,
        stderr=subprocess.PIPE, env=bash_environment,
        universal_newlines=True)
    outputs = proc.communicate()

    for line in outputs[1].split('\n'):
        m = r.match(line)
        if m:
            matches.append(m)

    for m in matches:
        if 'syntax error' in m.group('error'):
            msg = '%s: %s' % (MESSAGES['E040'].msg, m.group('error'))
            report.print_error(msg, filename=filename,
                               filelineno=int(m.group('lineno')))

        # Matching output from bash warning about here-documents not
        # ending.
        # FIXME: are there other warnings that might come out
        # with "bash -n"?  A quick scan of the source code suggests
        # no, but there might be other interesting things we could
        # catch.
        if 'warning:' in m.group('error'):
            if 'delimited by end-of-file' in m.group('error'):
                start = re.match('^.*line (?P<start>[0-9]+).*$',
                                 m.group('error'))
                report.print_error(
                    MESSAGES['E012'].msg % int(start.group('start')),
                    filename=filename,
                    filelineno=int(m.group('lineno')))


class BashateRun(object):

    def __init__(self):
        self.error_count = 0
        self.error_list = None
        self.ignore_list = None
        self.warning_count = 0
        self.warning_list = None

    def register_ignores(self, ignores):
        if ignores:
            self.ignore_list = '^(' + '|'.join(ignores.split(',')) + ')'

    def register_warnings(self, warnings):
        if warnings:
            self.warning_list = '^(' + '|'.join(warnings.split(',')) + ')'

    def register_errors(self, errors):
        if errors:
            self.error_list = '^(' + '|'.join(errors.split(',')) + ')'

    def should_ignore(self, error):
        return self.ignore_list and re.search(self.ignore_list, error)

    def should_warn(self, error):
        # if in the errors list, overrides warning level
        if self.error_list and re.search(self.error_list, error):
            return False
        if messages.is_default_warning(error):
            return True
        return self.warning_list and re.search(self.warning_list, error)

    def print_error(self, error, line='',
                    filename=None, filelineno=None):
        if self.should_ignore(error):
            return

        warn = self.should_warn(error)

        if not filename:
            filename = fileinput.filename()
        if not filelineno:
            filelineno = fileinput.filelineno()
        if warn:
            self.warning_count = self.warning_count + 1
        else:
            self.error_count = self.error_count + 1

        self.log_error(error, line, filename, filelineno, warn)

    def log_error(self, error, line, filename, filelineno, warn=False):
        print("[%(warn)s] %(error)s: '%(line)s'" %
              {'warn': "W" if warn else "E",
               'error': error,
               'line': line.rstrip('\n')})
        print(" - %s : L%s" % (filename, filelineno))

    def check_files(self, files, verbose):
        logical_line = ""
        token = False

        # NOTE(mrodden): magic; replace with proper
        # report class when necessary
        report = self

        for fname in files:

            # reset world
            in_heredoc = False
            in_continuation = False

            # simple syntax checking, as files can pass style but still cause
            # syntax errors when you try to run them.
            check_syntax(fname, report)

            for line in fileinput.input(fname):
                if fileinput.isfirstline():

                    check_hashbang(line, fileinput.filename(), report)

                    if verbose:
                        print("Running bashate on %s" % fileinput.filename())

                # Don't run any tests on comment lines (but remember
                # inside a heredoc this might be part of the syntax of
                # an embedded script, just ignore that)
                if line.lstrip().startswith('#') and not in_heredoc:
                    continue

                # Strip trailing comments. From bash:
                #
                #   a word beginning with # causes that word and all
                #   remaining characters on that line to be ignored.
                #   ...
                #   A character that, when unquoted, separates
                #   words. One of the following: | & ; ( ) < > space
                #   tab
                #
                # for simplicity, we strip inline comments by
                # matching just '<space>#'.
                if not in_heredoc:
                    ll_split = line.split(' #', 1)
                    if len(ll_split) > 1:
                        line = ll_split[0].rstrip()

                # see if this starts a heredoc
                if not in_heredoc:
                    token = starts_heredoc(line)
                    if token:
                        in_heredoc = True
                        logical_line = [line]
                        continue

                # see if this starts a continuation
                if not in_continuation:
                    if is_continuation(line):
                        in_continuation = True
                        logical_line = [line]
                        continue

                # if we are in a heredoc or continuation, just loop
                # back and keep buffering the lines into
                # "logical_line" until the end of the
                # heredoc/continuation.
                if in_heredoc:
                    logical_line.append(line)
                    if not end_of_heredoc(line, token):
                        continue
                    else:
                        in_heredoc = False
                        # FIXME: if we want to do something with
                        # heredocs in the future, then the whole thing
                        # is now stored in logical_line.  for now,
                        # skip
                        continue
                elif in_continuation:
                    logical_line.append(line)
                    if is_continuation(line):
                        continue
                    else:
                        in_continuation = False
                else:
                    logical_line = [line]

                check_indents(logical_line, report)

                # at this point, logical_line is an array that holds
                # the whole continuation.  XXX : historically, we've
                # just handled every line in a continuation
                # separatley.  Stick with what works...
                for line in logical_line:
                    check_no_trailing_whitespace(line, report)
                    check_no_long_lines(line, report)
                    check_for_do(line, report)
                    check_if_then(line, report)
                    check_function_decl(line, report)
                    check_arithmetic(line, report)
                    check_local_subshell(line, report)
                    check_bare_arithmetic(line, report)

        # finished processing the file

        # last line should always end with a newline
        if not line.endswith('\n'):
            report.print_error(MESSAGES['E004'].msg, line)


def main(args=None):

    if args is None:
        args = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description='A bash script style checker')
    parser.add_argument('files', metavar='file', nargs='*',
                        help='files to scan for errors')
    parser.add_argument('-i', '--ignore', help='Rules to ignore')
    parser.add_argument('-w', '--warn',
                        help='Rules to always warn (rather than error)')
    parser.add_argument('-e', '--error',
                        help='Rules to always error (rather than warn)')
    parser.add_argument('-v', '--verbose', action='store_true', default=False)
    parser.add_argument('-s', '--show', action='store_true', default=False)
    opts = parser.parse_args(args)

    if opts.show:
        messages.print_messages()
        sys.exit(0)

    files = opts.files
    if not files:
        parser.print_usage()
        return 1

    run = BashateRun()
    run.register_ignores(opts.ignore)
    run.register_warnings(opts.warn)
    run.register_errors(opts.error)

    try:
        run.check_files(files, opts.verbose)
    except IOError as e:
        print("bashate: %s" % e)
        return 1

    if run.warning_count > 0:
        print("%d bashate warning(s) found" % run.warning_count)

    if run.error_count > 0:
        print("%d bashate error(s) found" % run.error_count)
        return 1

    return 0

if __name__ == "__main__":
    sys.exit(main())