pureshellcheck 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pureshellcheck/__init__.py +58 -0
- pureshellcheck/analyzer.py +403 -0
- pureshellcheck/astlib.py +302 -0
- pureshellcheck/checks/__init__.py +6 -0
- pureshellcheck/checks/commands.py +1003 -0
- pureshellcheck/checks/misc.py +233 -0
- pureshellcheck/checks/quoting.py +440 -0
- pureshellcheck/checks/variables.py +348 -0
- pureshellcheck/cli.py +162 -0
- pureshellcheck/parser.py +2050 -0
- pureshellcheck/shast.py +106 -0
- pureshellcheck/varflow.py +584 -0
- pureshellcheck/varscan.py +511 -0
- pureshellcheck-0.1.0.dist-info/METADATA +184 -0
- pureshellcheck-0.1.0.dist-info/RECORD +19 -0
- pureshellcheck-0.1.0.dist-info/WHEEL +5 -0
- pureshellcheck-0.1.0.dist-info/entry_points.txt +2 -0
- pureshellcheck-0.1.0.dist-info/licenses/LICENSE +21 -0
- pureshellcheck-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Variable lifecycle and array checks."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ..analyzer import node_check, tree_check
|
|
6
|
+
from ..astlib import braced_modifier, braced_reference
|
|
7
|
+
from ..parser import literal_text
|
|
8
|
+
from ..shast import ancestors, walk
|
|
9
|
+
from ..varscan import NAME_RE, VarScan, levenshtein
|
|
10
|
+
|
|
11
|
+
INTERNAL_VARIABLES = frozenset({
|
|
12
|
+
"_", "rest", "REST", "CDPATH", "ENV", "FCEDIT", "HISTFILE", "HISTSIZE",
|
|
13
|
+
"HOME", "IFS", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
|
14
|
+
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "MAIL",
|
|
15
|
+
"MAILCHECK", "MAILPATH", "OLDPWD", "OPTARG", "OPTIND", "PATH", "PWD",
|
|
16
|
+
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
|
17
|
+
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
|
|
18
|
+
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",
|
|
19
|
+
"BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",
|
|
20
|
+
"BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",
|
|
21
|
+
"COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",
|
|
22
|
+
"EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS",
|
|
23
|
+
"HISTCMD", "HOSTNAME", "HOSTTYPE", "MACHTYPE", "MAPFILE", "OSTYPE",
|
|
24
|
+
"PIPESTATUS", "RANDOM", "READLINE_ARGUMENT", "READLINE_LINE",
|
|
25
|
+
"READLINE_MARK", "READLINE_POINT", "REPLY", "SECONDS", "SHELLOPTS",
|
|
26
|
+
"SHLVL", "SRANDOM", "UID", "BASH_COMPAT", "BASH_ENV", "BASH_XTRACEFD",
|
|
27
|
+
"CHILD_MAX", "COLUMNS", "COMPREPLY", "EMACS", "EXECIGNORE", "FIGNORE",
|
|
28
|
+
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILESIZE", "HISTIGNORE",
|
|
29
|
+
"HISTTIMEFORMAT", "HOSTFILE", "IGNOREEOF", "INPUTRC", "INSIDE_EMACS",
|
|
30
|
+
"LINES", "OPTERR", "POSIXLY_CORRECT", "PROMPT_COMMAND",
|
|
31
|
+
"PROMPT_DIRTRIM", "PS0", "PS1", "PS2", "PS3", "PS4", "SHELL",
|
|
32
|
+
"TIMEFORMAT", "TMOUT", "BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT",
|
|
33
|
+
"auto_resume", "histchars", "USER", "TZ", "TERM", "LOGNAME",
|
|
34
|
+
"LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY", "KRB5CCNAME", "LINENO",
|
|
35
|
+
"PPID", "TMPDIR", "XAUTHORITY", "FLAGS_ARGC", "FLAGS_ARGV",
|
|
36
|
+
"FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP", "FLAGS_PARENT",
|
|
37
|
+
"FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION", "flags_error",
|
|
38
|
+
"flags_return", "stderr", "stderr_lines", "status", "output", "lines",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
INTERNAL_ARRAYS = frozenset({
|
|
42
|
+
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
|
|
43
|
+
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS",
|
|
44
|
+
"COPROC", "DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS",
|
|
45
|
+
"COMPREPLY", "lines",
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
COMMON_COMMANDS_HINT = frozenset({
|
|
49
|
+
"cat", "cut", "date", "find", "grep", "head", "ls", "sed", "sort",
|
|
50
|
+
"tail", "wc", "whoami", "pwd", "dirname", "basename",
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_varscan(ctx):
|
|
55
|
+
scan = ctx.cache.get("varscan")
|
|
56
|
+
if scan is None:
|
|
57
|
+
scan = VarScan(ctx.root, ctx.shell)
|
|
58
|
+
ctx.cache["varscan"] = scan
|
|
59
|
+
return scan
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ----------------------------------------------------------------------
|
|
63
|
+
# SC2034: unused variables
|
|
64
|
+
|
|
65
|
+
@tree_check
|
|
66
|
+
def check_unused_assignments(ctx, root):
|
|
67
|
+
scan = get_varscan(ctx)
|
|
68
|
+
referenced = set()
|
|
69
|
+
prefixes = []
|
|
70
|
+
for ref in scan.refs:
|
|
71
|
+
if ref.kind == "prefix":
|
|
72
|
+
prefixes.append(ref.name)
|
|
73
|
+
else:
|
|
74
|
+
referenced.add(ref.name)
|
|
75
|
+
exported = {a.name for a in scan.assigns if a.exported}
|
|
76
|
+
first_assign = {}
|
|
77
|
+
for a in scan.assigns:
|
|
78
|
+
if a.kind == "checked":
|
|
79
|
+
referenced.add(a.name)
|
|
80
|
+
continue
|
|
81
|
+
if _is_env_prefix_assignment(a.node):
|
|
82
|
+
referenced.add(a.name)
|
|
83
|
+
continue
|
|
84
|
+
if a.name not in first_assign:
|
|
85
|
+
first_assign[a.name] = a
|
|
86
|
+
for name, a in sorted(first_assign.items(),
|
|
87
|
+
key=lambda kv: kv[1].node.pos):
|
|
88
|
+
if name in referenced or name in exported:
|
|
89
|
+
continue
|
|
90
|
+
if name.startswith("_"):
|
|
91
|
+
continue
|
|
92
|
+
if name in INTERNAL_VARIABLES or not NAME_RE.match(name):
|
|
93
|
+
continue
|
|
94
|
+
if any(name.startswith(p) for p in prefixes):
|
|
95
|
+
continue
|
|
96
|
+
ctx.warn(a.node, 2034, "%s appears unused. Verify use (or export"
|
|
97
|
+
" if used externally)." % name)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ----------------------------------------------------------------------
|
|
101
|
+
# SC2154 / SC2153: referenced but not assigned
|
|
102
|
+
|
|
103
|
+
@tree_check
|
|
104
|
+
def check_unassigned_references(ctx, root):
|
|
105
|
+
scan = get_varscan(ctx)
|
|
106
|
+
assigned = {a.name for a in scan.assigns}
|
|
107
|
+
written = sorted(a.name for a in scan.assigns
|
|
108
|
+
if a.kind != "checked" and NAME_RE.match(a.name))
|
|
109
|
+
seen = set()
|
|
110
|
+
for ref in scan.refs:
|
|
111
|
+
name = ref.name
|
|
112
|
+
if name in seen:
|
|
113
|
+
continue
|
|
114
|
+
if not NAME_RE.match(name):
|
|
115
|
+
continue
|
|
116
|
+
if name in assigned or name in INTERNAL_VARIABLES:
|
|
117
|
+
continue
|
|
118
|
+
if ref.kind in ("guarded", "index-of-other", "prefix"):
|
|
119
|
+
seen.add(name)
|
|
120
|
+
continue
|
|
121
|
+
seen.add(name)
|
|
122
|
+
if name.lower() == name or any(c.islower() for c in name):
|
|
123
|
+
match = _best_match(name, written)
|
|
124
|
+
tip = ""
|
|
125
|
+
if name in COMMON_COMMANDS_HINT:
|
|
126
|
+
tip = ' (for output from commands, use "$(%s ...)" )' % name
|
|
127
|
+
elif match:
|
|
128
|
+
tip = " (did you mean '%s'?)" % match
|
|
129
|
+
ctx.warn(ref.node, 2154,
|
|
130
|
+
"%s is referenced but not assigned%s." % (name, tip))
|
|
131
|
+
else:
|
|
132
|
+
match = _best_match(name, written)
|
|
133
|
+
if match:
|
|
134
|
+
ctx.info(ref.node, 2153, "Possible misspelling: %s may not"
|
|
135
|
+
" be assigned. Did you mean %s?" % (name, match))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _is_env_prefix_assignment(node):
|
|
139
|
+
"""FOO=bar somecommand: the assignment is for the command's env."""
|
|
140
|
+
if node.kind != "T_Assignment":
|
|
141
|
+
return False
|
|
142
|
+
parent = node.parent
|
|
143
|
+
if parent is None or parent.kind != "T_SimpleCommand":
|
|
144
|
+
return False
|
|
145
|
+
if node not in parent.assigns or not parent.words:
|
|
146
|
+
return False
|
|
147
|
+
from ..parser import literal_text
|
|
148
|
+
cmd = literal_text(parent.words[0])
|
|
149
|
+
if cmd:
|
|
150
|
+
cmd = cmd.rsplit("/", 1)[-1]
|
|
151
|
+
return cmd not in ("declare", "typeset", "local", "export", "readonly")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _best_match(var, candidates):
|
|
155
|
+
best = None
|
|
156
|
+
best_score = 99
|
|
157
|
+
for c in candidates:
|
|
158
|
+
if c == var:
|
|
159
|
+
continue
|
|
160
|
+
if c.lower() == var.lower():
|
|
161
|
+
score = 1
|
|
162
|
+
elif abs(len(c) - len(var)) > 2:
|
|
163
|
+
continue
|
|
164
|
+
else:
|
|
165
|
+
score = levenshtein(var, c)
|
|
166
|
+
if score < best_score:
|
|
167
|
+
best, best_score = c, score
|
|
168
|
+
if best is None:
|
|
169
|
+
return None
|
|
170
|
+
if len(best) > 7 and best_score <= 2:
|
|
171
|
+
return best
|
|
172
|
+
if len(best) > 3 and best_score <= 1:
|
|
173
|
+
return best
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ----------------------------------------------------------------------
|
|
178
|
+
# SC2004: $ on arithmetic variables
|
|
179
|
+
|
|
180
|
+
@node_check("TA_Expansion")
|
|
181
|
+
def check_arithmetic_deref(ctx, node):
|
|
182
|
+
if len(node.parts) != 1 or node.parts[0].kind != "T_DollarBraced":
|
|
183
|
+
return
|
|
184
|
+
braced = node.parts[0]
|
|
185
|
+
content = braced.content
|
|
186
|
+
name = braced_reference(content)
|
|
187
|
+
if not NAME_RE.match(name):
|
|
188
|
+
return
|
|
189
|
+
if braced_modifier(content) or content.startswith(("#", "!")):
|
|
190
|
+
return
|
|
191
|
+
if "[" in content:
|
|
192
|
+
return
|
|
193
|
+
if _in_let(node):
|
|
194
|
+
return
|
|
195
|
+
ctx.style(braced, 2004, "$/${} is unnecessary on arithmetic"
|
|
196
|
+
" variables.")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@tree_check
|
|
200
|
+
def check_assignment_index_deref(ctx, root):
|
|
201
|
+
# a[$i]=foo for indexed arrays
|
|
202
|
+
scan = get_varscan(ctx)
|
|
203
|
+
for node in walk(root):
|
|
204
|
+
if node.kind != "T_Assignment":
|
|
205
|
+
continue
|
|
206
|
+
if node.name in scan.assoc_arrays:
|
|
207
|
+
continue
|
|
208
|
+
for idx in node.get("indices", ()):
|
|
209
|
+
if isinstance(idx, str) \
|
|
210
|
+
and re.fullmatch(r"\$\{?[A-Za-z_][A-Za-z0-9_]*\}?",
|
|
211
|
+
idx.strip()):
|
|
212
|
+
ctx.style(node, 2004, "$/${} is unnecessary on arithmetic"
|
|
213
|
+
" variables.")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _in_let(node):
|
|
217
|
+
for a in ancestors(node):
|
|
218
|
+
if a.kind == "T_SimpleCommand":
|
|
219
|
+
name = literal_text(a.words[0]) if a.words else None
|
|
220
|
+
return name == "let"
|
|
221
|
+
if a.kind in ("T_Arithmetic", "T_DollarArithmetic", "T_Script",
|
|
222
|
+
"T_Condition"):
|
|
223
|
+
return False
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@node_check("T_Arithmetic", "T_DollarArithmetic")
|
|
228
|
+
def check_array_index_deref(ctx, node):
|
|
229
|
+
# (( a[$i] )) -- $ on the index variable
|
|
230
|
+
scan = get_varscan(ctx)
|
|
231
|
+
for n in walk(node):
|
|
232
|
+
if n.kind != "TA_Variable":
|
|
233
|
+
continue
|
|
234
|
+
if n.name in scan.assoc_arrays:
|
|
235
|
+
continue
|
|
236
|
+
for idx in n.get("indices", ()):
|
|
237
|
+
if idx.kind == "TA_Expansion" and len(idx.parts) == 1 \
|
|
238
|
+
and idx.parts[0].kind == "T_DollarBraced":
|
|
239
|
+
content = idx.parts[0].content
|
|
240
|
+
if NAME_RE.match(braced_reference(content)) \
|
|
241
|
+
and not braced_modifier(content) \
|
|
242
|
+
and not content.startswith(("#", "!")):
|
|
243
|
+
ctx.style(idx.parts[0], 2004, "$/${} is unnecessary on"
|
|
244
|
+
" arithmetic variables.")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ----------------------------------------------------------------------
|
|
248
|
+
# SC2128 / SC2178 / SC2179: arrays used as strings
|
|
249
|
+
|
|
250
|
+
@tree_check
|
|
251
|
+
def check_array_without_index(ctx, root):
|
|
252
|
+
scan = get_varscan(ctx)
|
|
253
|
+
events = [] # (pos, type, name, node)
|
|
254
|
+
for a in scan.assigns:
|
|
255
|
+
if not NAME_RE.match(a.name):
|
|
256
|
+
continue
|
|
257
|
+
node = a.node
|
|
258
|
+
if node.kind == "T_Assignment":
|
|
259
|
+
value = node.get("value")
|
|
260
|
+
is_arr = value is not None and value.kind == "T_Array"
|
|
261
|
+
if node.get("indices"):
|
|
262
|
+
is_arr = True
|
|
263
|
+
events.append((node.end, "array" if is_arr else "string",
|
|
264
|
+
a.name, node, a.append))
|
|
265
|
+
elif a.is_array:
|
|
266
|
+
events.append((node.end, "array", a.name, node, False))
|
|
267
|
+
for ref in scan.refs:
|
|
268
|
+
node = ref.node
|
|
269
|
+
if node.kind != "T_DollarBraced":
|
|
270
|
+
continue
|
|
271
|
+
content = node.content
|
|
272
|
+
if content != ref.name:
|
|
273
|
+
continue # only plain $var / ${var}
|
|
274
|
+
events.append((node.pos, "ref", ref.name, node, False))
|
|
275
|
+
events.sort(key=lambda e: e[0])
|
|
276
|
+
arrays = set(INTERNAL_ARRAYS)
|
|
277
|
+
warned = set()
|
|
278
|
+
for pos, etype, name, node, append in events:
|
|
279
|
+
if etype == "array":
|
|
280
|
+
arrays.add(name)
|
|
281
|
+
elif etype == "string":
|
|
282
|
+
if name in arrays:
|
|
283
|
+
if (name, pos) in warned:
|
|
284
|
+
continue
|
|
285
|
+
warned.add((name, pos))
|
|
286
|
+
if append:
|
|
287
|
+
ctx.warn(node, 2179, 'Use array+=("item") to append'
|
|
288
|
+
" items to an array.")
|
|
289
|
+
else:
|
|
290
|
+
ctx.warn(node, 2178, "Variable was used as an array"
|
|
291
|
+
" but is now assigned a string.")
|
|
292
|
+
arrays.discard(name)
|
|
293
|
+
else:
|
|
294
|
+
if name in arrays:
|
|
295
|
+
if name in warned:
|
|
296
|
+
continue
|
|
297
|
+
warned.add(name)
|
|
298
|
+
ctx.warn(node, 2128, "Expanding an array without an index"
|
|
299
|
+
" only gives the first element.")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ----------------------------------------------------------------------
|
|
303
|
+
# SC2155: declare and assign separately
|
|
304
|
+
|
|
305
|
+
@node_check("T_SimpleCommand")
|
|
306
|
+
def check_masked_returns(ctx, node):
|
|
307
|
+
if not node.words or not node.assigns:
|
|
308
|
+
return
|
|
309
|
+
name = literal_text(node.words[0])
|
|
310
|
+
if name:
|
|
311
|
+
name = name.rsplit("/", 1)[-1]
|
|
312
|
+
if name not in ("declare", "typeset", "local", "readonly", "export"):
|
|
313
|
+
return
|
|
314
|
+
flags = ""
|
|
315
|
+
for w in node.words[1:]:
|
|
316
|
+
t = literal_text(w)
|
|
317
|
+
if t and t.startswith("-"):
|
|
318
|
+
flags += t[1:]
|
|
319
|
+
is_scoped = _in_scoped_function(ctx, node)
|
|
320
|
+
is_local = (name in ("local", "declare", "typeset")
|
|
321
|
+
and "g" not in flags and is_scoped)
|
|
322
|
+
is_readonly = name == "readonly" or "r" in flags
|
|
323
|
+
if is_local and is_readonly:
|
|
324
|
+
return
|
|
325
|
+
from ..astlib import expanded_parts
|
|
326
|
+
for assign in node.assigns:
|
|
327
|
+
value = assign.get("value")
|
|
328
|
+
if value is None:
|
|
329
|
+
continue
|
|
330
|
+
for p in expanded_parts(value):
|
|
331
|
+
if p.kind in ("T_DollarExpansion", "T_Backticked",
|
|
332
|
+
"T_DollarBraceCommandExpansion"):
|
|
333
|
+
ctx.warn(assign, 2155, "Declare and assign separately to"
|
|
334
|
+
" avoid masking return values.")
|
|
335
|
+
break
|
|
336
|
+
else:
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _in_scoped_function(ctx, node):
|
|
341
|
+
for a in ancestors(node):
|
|
342
|
+
if a.kind == "T_BatsTest":
|
|
343
|
+
return True
|
|
344
|
+
if a.kind == "T_Function":
|
|
345
|
+
if ctx.shell == "ksh":
|
|
346
|
+
return a.get("keyword_form", False)
|
|
347
|
+
return True
|
|
348
|
+
return False
|
pureshellcheck/cli.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Command line interface, modeled on shellcheck's."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__, run_checks
|
|
8
|
+
|
|
9
|
+
SEVERITY_RANK = {"error": 0, "warning": 1, "info": 2, "style": 3}
|
|
10
|
+
|
|
11
|
+
COLORS = {
|
|
12
|
+
"error": "\x1b[1;31m",
|
|
13
|
+
"warning": "\x1b[1;33m",
|
|
14
|
+
"info": "\x1b[1;32m",
|
|
15
|
+
"style": "\x1b[1;32m",
|
|
16
|
+
"message": "\x1b[1m",
|
|
17
|
+
"source": "",
|
|
18
|
+
"reset": "\x1b[0m",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_args(argv):
|
|
23
|
+
p = argparse.ArgumentParser(
|
|
24
|
+
prog="pureshellcheck",
|
|
25
|
+
description="Lint shell scripts (pure Python port of ShellCheck's"
|
|
26
|
+
" most common checks)")
|
|
27
|
+
p.add_argument("files", nargs="+", metavar="FILE",
|
|
28
|
+
help="script files, or - for stdin")
|
|
29
|
+
p.add_argument("-s", "--shell",
|
|
30
|
+
choices=["sh", "bash", "dash", "ash", "ksh", "busybox"],
|
|
31
|
+
help="specify dialect (default: detect from shebang)")
|
|
32
|
+
p.add_argument("-f", "--format", default="tty",
|
|
33
|
+
choices=["tty", "gcc", "json", "json1"],
|
|
34
|
+
help="output format (default: tty)")
|
|
35
|
+
p.add_argument("-e", "--exclude", action="append", default=[],
|
|
36
|
+
metavar="CODE1,CODE2..", help="exclude these checks")
|
|
37
|
+
p.add_argument("-S", "--severity", default="style",
|
|
38
|
+
choices=["error", "warning", "info", "style"],
|
|
39
|
+
help="minimum severity to report (default: style)")
|
|
40
|
+
p.add_argument("-o", "--enable", action="append", default=[],
|
|
41
|
+
metavar="check1,check2..",
|
|
42
|
+
help="enable optional checks ('all' for every one)")
|
|
43
|
+
p.add_argument("-C", "--color", nargs="?", const="always",
|
|
44
|
+
default="auto", choices=["auto", "always", "never"],
|
|
45
|
+
help="use color (default: auto)")
|
|
46
|
+
p.add_argument("--version", action="version",
|
|
47
|
+
version="pureshellcheck %s" % __version__)
|
|
48
|
+
return p.parse_args(argv)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_excludes(items):
|
|
52
|
+
codes = set()
|
|
53
|
+
for item in items:
|
|
54
|
+
for part in item.split(","):
|
|
55
|
+
part = part.strip()
|
|
56
|
+
if part.upper().startswith("SC"):
|
|
57
|
+
part = part[2:]
|
|
58
|
+
if part.isdigit():
|
|
59
|
+
codes.add(int(part))
|
|
60
|
+
return codes
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_file(path, args, excluded, min_rank):
|
|
64
|
+
if path == "-":
|
|
65
|
+
source = sys.stdin.read()
|
|
66
|
+
name = "-"
|
|
67
|
+
else:
|
|
68
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
69
|
+
source = f.read()
|
|
70
|
+
name = path
|
|
71
|
+
shell = args.shell if args.shell != "busybox" else "ash"
|
|
72
|
+
include_optional = bool(args.enable)
|
|
73
|
+
findings, _err = run_checks(source, shell=shell,
|
|
74
|
+
include_optional=include_optional,
|
|
75
|
+
filename=name)
|
|
76
|
+
findings = [f for f in findings
|
|
77
|
+
if f.code not in excluded
|
|
78
|
+
and SEVERITY_RANK[f.severity] <= min_rank]
|
|
79
|
+
return name, source, findings
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def emit_tty(name, source, findings, out, color):
|
|
83
|
+
lines = source.split("\n")
|
|
84
|
+
c = COLORS if color else dict.fromkeys(COLORS, "")
|
|
85
|
+
for f in findings:
|
|
86
|
+
line_text = lines[f.line - 1] if f.line - 1 < len(lines) else ""
|
|
87
|
+
out.write("\n%sIn %s line %d:%s\n"
|
|
88
|
+
% (c["message"], name, f.line, c["reset"]))
|
|
89
|
+
out.write(line_text + "\n")
|
|
90
|
+
start = f.column - 1
|
|
91
|
+
if f.end_line == f.line and f.end_column > f.column:
|
|
92
|
+
width = f.end_column - f.column
|
|
93
|
+
else:
|
|
94
|
+
width = 1
|
|
95
|
+
marker = "^" if width <= 1 else "^" + "-" * (width - 2) + "^"
|
|
96
|
+
out.write(" " * start + "%s%s SC%d (%s): %s%s\n"
|
|
97
|
+
% (c[f.severity], marker, f.code, f.severity, f.message,
|
|
98
|
+
c["reset"]))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def emit_gcc(name, source, findings, out, color):
|
|
102
|
+
sev = {"error": "error", "warning": "warning", "info": "note",
|
|
103
|
+
"style": "note"}
|
|
104
|
+
for f in findings:
|
|
105
|
+
out.write("%s:%d:%d: %s: %s [SC%d]\n"
|
|
106
|
+
% (name, f.line, f.column, sev[f.severity], f.message,
|
|
107
|
+
f.code))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def finding_json(name, f):
|
|
111
|
+
return {
|
|
112
|
+
"file": name,
|
|
113
|
+
"line": f.line,
|
|
114
|
+
"endLine": f.end_line,
|
|
115
|
+
"column": f.column,
|
|
116
|
+
"endColumn": f.end_column,
|
|
117
|
+
"level": f.severity,
|
|
118
|
+
"code": f.code,
|
|
119
|
+
"message": f.message,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(argv=None):
|
|
124
|
+
args = parse_args(sys.argv[1:] if argv is None else argv)
|
|
125
|
+
excluded = parse_excludes(args.exclude)
|
|
126
|
+
min_rank = SEVERITY_RANK[args.severity]
|
|
127
|
+
out = sys.stdout
|
|
128
|
+
color = args.color == "always" or (args.color == "auto"
|
|
129
|
+
and out.isatty())
|
|
130
|
+
any_findings = False
|
|
131
|
+
had_error = False
|
|
132
|
+
json_items = []
|
|
133
|
+
for path in args.files:
|
|
134
|
+
try:
|
|
135
|
+
name, source, findings = check_file(path, args, excluded,
|
|
136
|
+
min_rank)
|
|
137
|
+
except OSError as e:
|
|
138
|
+
sys.stderr.write("pureshellcheck: %s: %s\n"
|
|
139
|
+
% (path, e.strerror or e))
|
|
140
|
+
had_error = True
|
|
141
|
+
continue
|
|
142
|
+
if findings:
|
|
143
|
+
any_findings = True
|
|
144
|
+
if args.format == "tty":
|
|
145
|
+
emit_tty(name, source, findings, out, color)
|
|
146
|
+
elif args.format == "gcc":
|
|
147
|
+
emit_gcc(name, source, findings, out, color)
|
|
148
|
+
else:
|
|
149
|
+
json_items.extend(finding_json(name, f) for f in findings)
|
|
150
|
+
if args.format == "json":
|
|
151
|
+
json.dump(json_items, out)
|
|
152
|
+
out.write("\n")
|
|
153
|
+
elif args.format == "json1":
|
|
154
|
+
json.dump({"comments": json_items}, out)
|
|
155
|
+
out.write("\n")
|
|
156
|
+
if had_error:
|
|
157
|
+
return 2
|
|
158
|
+
return 1 if any_findings else 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
sys.exit(main())
|