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,1003 @@
|
|
|
1
|
+
"""Command-specific checks (useless cat, pipe pitfalls, printf, etc.)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ..analyzer import node_check, tree_check
|
|
6
|
+
from ..astlib import is_constant, word_parts
|
|
7
|
+
from ..parser import literal_text, quoted_literal_text
|
|
8
|
+
from ..shast import ancestors, walk
|
|
9
|
+
from .quoting import may_become_multiple_args, will_split
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def simple_words(ctx, cmd):
|
|
13
|
+
return [literal_text(w) for w in cmd.words]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def first_word_basename(cmd):
|
|
17
|
+
if cmd.kind != "T_SimpleCommand" or not cmd.words:
|
|
18
|
+
return None
|
|
19
|
+
name = literal_text(cmd.words[0])
|
|
20
|
+
return name.rsplit("/", 1)[-1] if name else None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def pipeline_command_names(ctx, pipeline):
|
|
24
|
+
out = []
|
|
25
|
+
for c in pipeline.commands:
|
|
26
|
+
out.append(ctx.command_basename(c) if c.kind == "T_SimpleCommand"
|
|
27
|
+
else None)
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def word_approx(word):
|
|
32
|
+
"""Loose textual rendering of a word (expansions become empty)."""
|
|
33
|
+
parts = word.parts if word.kind in ("T_NormalWord", "T_DoubleQuoted",
|
|
34
|
+
"T_DollarDoubleQuoted") \
|
|
35
|
+
else [word]
|
|
36
|
+
out = []
|
|
37
|
+
for p in parts:
|
|
38
|
+
k = p.kind
|
|
39
|
+
if k == "T_Literal":
|
|
40
|
+
out.append(p.text)
|
|
41
|
+
elif k in ("T_SingleQuoted", "T_DollarSingleQuoted"):
|
|
42
|
+
out.append(p.text)
|
|
43
|
+
elif k in ("T_DoubleQuoted", "T_DollarDoubleQuoted"):
|
|
44
|
+
out.append(word_approx(p))
|
|
45
|
+
elif k == "T_Glob":
|
|
46
|
+
out.append(p.text)
|
|
47
|
+
else:
|
|
48
|
+
out.append("")
|
|
49
|
+
return "".join(out)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def flags_of(ctx, cmd):
|
|
53
|
+
return [f for f, w in ctx.flags(cmd)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ----------------------------------------------------------------------
|
|
57
|
+
# SC2002: useless cat
|
|
58
|
+
|
|
59
|
+
@node_check("T_Pipeline")
|
|
60
|
+
def check_uuoc(ctx, node):
|
|
61
|
+
first = node.commands[0]
|
|
62
|
+
if first.kind != "T_SimpleCommand":
|
|
63
|
+
return
|
|
64
|
+
if ctx.command_basename(first) != "cat":
|
|
65
|
+
return
|
|
66
|
+
args = ctx.argument_words(first)
|
|
67
|
+
if len(args) != 1:
|
|
68
|
+
return
|
|
69
|
+
word = args[0]
|
|
70
|
+
lit = literal_text(word)
|
|
71
|
+
if lit is not None and lit.startswith("-"):
|
|
72
|
+
return
|
|
73
|
+
if may_become_multiple_args(word):
|
|
74
|
+
return
|
|
75
|
+
# `cat $var` may expand to multiple files or flags; only warn when the
|
|
76
|
+
# argument is a single fixed file
|
|
77
|
+
if any(p.kind not in ("T_Literal", "T_SingleQuoted", "T_DoubleQuoted",
|
|
78
|
+
"T_DollarSingleQuoted")
|
|
79
|
+
for p in word_parts(word)):
|
|
80
|
+
return
|
|
81
|
+
for p in word_parts(word):
|
|
82
|
+
if p.kind == "T_Literal" and not is_constant(word):
|
|
83
|
+
return
|
|
84
|
+
if not is_constant(word):
|
|
85
|
+
# quoted expansions are fine to warn about, unquoted are not
|
|
86
|
+
quoted = word_parts(word)
|
|
87
|
+
if not all(p.kind in ("T_DoubleQuoted", "T_SingleQuoted",
|
|
88
|
+
"T_DollarSingleQuoted") for p in quoted):
|
|
89
|
+
return
|
|
90
|
+
for p in quoted:
|
|
91
|
+
if p.kind == "T_DoubleQuoted":
|
|
92
|
+
for q in p.parts:
|
|
93
|
+
if q.kind == "T_DollarBraced" \
|
|
94
|
+
and q.content.startswith("!"):
|
|
95
|
+
return
|
|
96
|
+
ctx.style(word, 2002, "Useless cat. Consider 'cmd < file | ..' or"
|
|
97
|
+
" 'cmd file | ..' instead.")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ----------------------------------------------------------------------
|
|
101
|
+
# SC2009/2010/2011/2012/2038/2126: pipe pitfalls
|
|
102
|
+
|
|
103
|
+
@node_check("T_Pipeline")
|
|
104
|
+
def check_pipe_pitfalls(ctx, node):
|
|
105
|
+
cmds = node.commands
|
|
106
|
+
names = []
|
|
107
|
+
for c in cmds:
|
|
108
|
+
names.append(first_word_basename(c))
|
|
109
|
+
|
|
110
|
+
def find_seq(seq):
|
|
111
|
+
hits = []
|
|
112
|
+
for i in range(len(names) - len(seq) + 1):
|
|
113
|
+
if all(s == "?" or names[i + j] == s
|
|
114
|
+
for j, s in enumerate(seq)):
|
|
115
|
+
hits.append(i)
|
|
116
|
+
return hits
|
|
117
|
+
|
|
118
|
+
def args_approx(cmd):
|
|
119
|
+
return [word_approx(w) for w in cmd.words[1:]]
|
|
120
|
+
|
|
121
|
+
def has_short(arglist, ch):
|
|
122
|
+
return any(a.startswith("-") and not a.startswith("--") and ch in a
|
|
123
|
+
for a in arglist)
|
|
124
|
+
|
|
125
|
+
def has_long(arglist, name):
|
|
126
|
+
return any(a.lstrip("-").startswith(name) for a in arglist)
|
|
127
|
+
|
|
128
|
+
for i in find_seq(["find", "xargs"]):
|
|
129
|
+
find_cmd, xargs_cmd = cmds[i], cmds[i + 1]
|
|
130
|
+
all_args = args_approx(xargs_cmd) + args_approx(find_cmd)
|
|
131
|
+
if not (has_short(all_args, "0") or has_long(all_args, "null")
|
|
132
|
+
or has_long(all_args, "print0")
|
|
133
|
+
or has_long(all_args, "printf")):
|
|
134
|
+
ctx.warn(find_cmd, 2038, "Use 'find .. -print0 | xargs -0 ..'"
|
|
135
|
+
" or 'find .. -exec .. +' to allow non-alphanumeric"
|
|
136
|
+
" filenames.")
|
|
137
|
+
|
|
138
|
+
for i in find_seq(["ps", "grep"]):
|
|
139
|
+
ps_cmd = cmds[i]
|
|
140
|
+
ps_flags = flags_of(ctx, ps_cmd)
|
|
141
|
+
if not any(f in ("p", "pid", "q", "quick-pid") for f in ps_flags):
|
|
142
|
+
ctx.info(ps_cmd, 2009, "Consider using pgrep instead of"
|
|
143
|
+
" grepping ps output.")
|
|
144
|
+
|
|
145
|
+
for i in find_seq(["grep", "wc"]):
|
|
146
|
+
grep_cmd, wc_cmd = cmds[i], cmds[i + 1]
|
|
147
|
+
grep_flags = flags_of(ctx, grep_cmd)
|
|
148
|
+
wc_flags = flags_of(ctx, wc_cmd)
|
|
149
|
+
if not (any(f in ("l", "files-with-matches", "L",
|
|
150
|
+
"files-without-matches", "o", "only-matching",
|
|
151
|
+
"r", "R", "recursive", "A", "after-context",
|
|
152
|
+
"B", "before-context") for f in grep_flags)
|
|
153
|
+
or any(f in ("m", "chars", "w", "words", "c", "bytes",
|
|
154
|
+
"L", "max-line-length") for f in wc_flags)
|
|
155
|
+
or not wc_flags):
|
|
156
|
+
ctx.style(grep_cmd, 2126, "Consider using 'grep -c' instead"
|
|
157
|
+
" of 'grep|wc -l'.")
|
|
158
|
+
|
|
159
|
+
did_ls = False
|
|
160
|
+
for i in find_seq(["ls", "grep"]):
|
|
161
|
+
did_ls = True
|
|
162
|
+
ctx.warn(cmds[i], 2010, "Don't use ls | grep. Use a glob or a"
|
|
163
|
+
" for loop with a condition to allow non-alphanumeric"
|
|
164
|
+
" filenames.")
|
|
165
|
+
for i in find_seq(["ls", "xargs"]):
|
|
166
|
+
did_ls = True
|
|
167
|
+
ctx.warn(cmds[i], 2011, "Use 'find .. -print0 | xargs -0 ..' or"
|
|
168
|
+
" 'find .. -exec .. +' to allow non-alphanumeric"
|
|
169
|
+
" filenames.")
|
|
170
|
+
if not did_ls:
|
|
171
|
+
for i in find_seq(["ls", "?"]):
|
|
172
|
+
if not has_short(args_approx(cmds[i]), "N"):
|
|
173
|
+
ctx.info(cmds[i], 2012, "Use find instead of ls to better"
|
|
174
|
+
" handle non-alphanumeric filenames.")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ----------------------------------------------------------------------
|
|
178
|
+
# SC2005: echo $(cmd); SC2116: cmd $(echo foo)
|
|
179
|
+
|
|
180
|
+
@node_check("T_SimpleCommand")
|
|
181
|
+
def check_uuoe_cmd(ctx, node):
|
|
182
|
+
if first_word_basename(node) != "echo":
|
|
183
|
+
return
|
|
184
|
+
args = node.words[1:]
|
|
185
|
+
if len(args) != 1:
|
|
186
|
+
return
|
|
187
|
+
if _is_just_command_output(args[0]):
|
|
188
|
+
ctx.style(node, 2005, "Useless echo? Instead of 'echo $(cmd)',"
|
|
189
|
+
" just use 'cmd'.")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@node_check("T_DollarExpansion", "T_Backticked")
|
|
193
|
+
def check_uuoe_var(ctx, node):
|
|
194
|
+
if len(node.commands) != 1:
|
|
195
|
+
return
|
|
196
|
+
cmd = node.commands[0]
|
|
197
|
+
if cmd.kind != "T_SimpleCommand" or cmd.get("redirects"):
|
|
198
|
+
return
|
|
199
|
+
if first_word_basename(cmd) != "echo":
|
|
200
|
+
return
|
|
201
|
+
args = cmd.words[1:]
|
|
202
|
+
if not args:
|
|
203
|
+
return
|
|
204
|
+
lit0 = literal_text(args[0])
|
|
205
|
+
if lit0 is not None and lit0.startswith("-"):
|
|
206
|
+
return
|
|
207
|
+
if len(args) == 1 and _is_just_command_output(args[0]):
|
|
208
|
+
return
|
|
209
|
+
for a in args:
|
|
210
|
+
if not _could_be_optimized(a):
|
|
211
|
+
return
|
|
212
|
+
ctx.style(node, 2116, "Useless echo? Instead of 'cmd $(echo foo)',"
|
|
213
|
+
" just use 'cmd foo'.")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _could_be_optimized(node):
|
|
217
|
+
k = node.kind
|
|
218
|
+
if k in ("T_Glob", "T_Extglob", "T_BraceExpansion"):
|
|
219
|
+
return False
|
|
220
|
+
if k in ("T_NormalWord", "T_DoubleQuoted"):
|
|
221
|
+
return all(_could_be_optimized(p) for p in node.parts)
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _is_just_command_output(word):
|
|
226
|
+
parts = word_parts(word)
|
|
227
|
+
if len(parts) != 1:
|
|
228
|
+
return False
|
|
229
|
+
p = parts[0]
|
|
230
|
+
if p.kind == "T_DoubleQuoted" and len(p.parts) == 1:
|
|
231
|
+
p = p.parts[0]
|
|
232
|
+
if p.kind not in ("T_DollarExpansion", "T_Backticked"):
|
|
233
|
+
return False
|
|
234
|
+
return any(_has_words(c) for c in p.commands)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _has_words(node):
|
|
238
|
+
if node.kind == "T_SimpleCommand":
|
|
239
|
+
return bool(node.words)
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ----------------------------------------------------------------------
|
|
244
|
+
# SC2162: read without -r
|
|
245
|
+
|
|
246
|
+
@node_check("T_SimpleCommand")
|
|
247
|
+
def check_read_without_r(ctx, node):
|
|
248
|
+
if ctx.command_basename(node) != "read":
|
|
249
|
+
return
|
|
250
|
+
flags = ctx.flags(node)
|
|
251
|
+
names = [f for f, w in flags]
|
|
252
|
+
if "r" in names:
|
|
253
|
+
return
|
|
254
|
+
# read -t 0 only checks whether input is available
|
|
255
|
+
args = [literal_text(w) for w in ctx.argument_words(node)]
|
|
256
|
+
for i, a in enumerate(args):
|
|
257
|
+
if a == "-t" and i + 1 < len(args) and args[i + 1] == "0":
|
|
258
|
+
return
|
|
259
|
+
if a and a.startswith("-t") and a[2:] == "0":
|
|
260
|
+
return
|
|
261
|
+
ctx.info(node, 2162, "read without -r will mangle backslashes.")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ----------------------------------------------------------------------
|
|
265
|
+
# SC2164: unchecked cd/pushd/popd
|
|
266
|
+
|
|
267
|
+
SAFE_DIR_RE = re.compile(r"^/*((\.|\.\.)/+)*(\.|\.\.)?$")
|
|
268
|
+
SET_E_SHEBANG_RE = re.compile(r"[ \t]-[^-\s]*e")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def has_set_e(ctx):
|
|
272
|
+
cached = ctx.cache.get("has_set_e")
|
|
273
|
+
if cached is not None:
|
|
274
|
+
return cached
|
|
275
|
+
result = False
|
|
276
|
+
shebang = ctx.root.get("shebang") or ""
|
|
277
|
+
if SET_E_SHEBANG_RE.search(shebang):
|
|
278
|
+
result = True
|
|
279
|
+
else:
|
|
280
|
+
for node in walk(ctx.root):
|
|
281
|
+
if node.kind != "T_SimpleCommand" or not node.words:
|
|
282
|
+
continue
|
|
283
|
+
if first_word_basename(node) != "set":
|
|
284
|
+
continue
|
|
285
|
+
words = [literal_text(w) for w in node.words[1:]]
|
|
286
|
+
if any(w == "errexit" for w in words) or \
|
|
287
|
+
any(w and w.startswith("-") and not w.startswith("--")
|
|
288
|
+
and "e" in w for w in words):
|
|
289
|
+
result = True
|
|
290
|
+
break
|
|
291
|
+
ctx.cache["has_set_e"] = result
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def is_condition(node):
|
|
296
|
+
"""Is this node's exit status checked by a conditional construct?"""
|
|
297
|
+
prev = node
|
|
298
|
+
for a in ancestors(node):
|
|
299
|
+
k = a.kind
|
|
300
|
+
if k == "T_BatsTest":
|
|
301
|
+
return True
|
|
302
|
+
if k in ("T_AndIf", "T_OrIf"):
|
|
303
|
+
if prev is a.left:
|
|
304
|
+
return True
|
|
305
|
+
elif k == "T_IfExpression":
|
|
306
|
+
for cond, _body in a.branches:
|
|
307
|
+
if cond and prev is cond[-1]:
|
|
308
|
+
return True
|
|
309
|
+
elif k in ("T_WhileExpression", "T_UntilExpression"):
|
|
310
|
+
if a.condition and prev is a.condition[-1]:
|
|
311
|
+
return True
|
|
312
|
+
elif k in ("T_Banged", "T_Pipeline", "T_Timed"):
|
|
313
|
+
pass
|
|
314
|
+
elif k in ("T_Script", "T_SimpleCommand", "T_DollarExpansion",
|
|
315
|
+
"T_Backticked", "T_Subshell", "T_BraceGroup",
|
|
316
|
+
"T_Function"):
|
|
317
|
+
return False
|
|
318
|
+
prev = a
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@node_check("T_SimpleCommand")
|
|
323
|
+
def check_unchecked_cd(ctx, node):
|
|
324
|
+
name = ctx.command_basename(node)
|
|
325
|
+
if name not in ("cd", "pushd", "popd"):
|
|
326
|
+
return
|
|
327
|
+
if has_set_e(ctx):
|
|
328
|
+
return
|
|
329
|
+
args = ctx.argument_words(node)
|
|
330
|
+
flags = [f for f, w in ctx.flags(node)]
|
|
331
|
+
if name in ("pushd", "popd") and "n" in flags:
|
|
332
|
+
return
|
|
333
|
+
non_flag = [literal_text(w) for w in args
|
|
334
|
+
if not (literal_text(w) or "").startswith("-")
|
|
335
|
+
or literal_text(w) is None]
|
|
336
|
+
if len(args) == 1 and len(non_flag) == 1 and non_flag[0] is not None \
|
|
337
|
+
and SAFE_DIR_RE.match(non_flag[0]):
|
|
338
|
+
return
|
|
339
|
+
if is_condition(node):
|
|
340
|
+
return
|
|
341
|
+
if _is_last_command_in_function(node):
|
|
342
|
+
return
|
|
343
|
+
ctx.warn(node, 2164, "Use '%s ... || exit' or '%s ... || return' in"
|
|
344
|
+
" case %s fails." % (name, name, name))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _is_last_command_in_function(node):
|
|
348
|
+
prev = node
|
|
349
|
+
for a in ancestors(node):
|
|
350
|
+
if a.kind == "T_BraceGroup":
|
|
351
|
+
parent = a.parent
|
|
352
|
+
if parent is not None and parent.kind == "T_Function":
|
|
353
|
+
cmds = a.commands
|
|
354
|
+
return bool(cmds) and cmds[-1] is prev
|
|
355
|
+
if a.kind in ("T_Script", "T_Subshell", "T_DollarExpansion"):
|
|
356
|
+
return False
|
|
357
|
+
prev = a
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ----------------------------------------------------------------------
|
|
362
|
+
# SC2059 / SC2182 / SC2183: printf
|
|
363
|
+
|
|
364
|
+
PRINTF_FORMAT_RE = re.compile(
|
|
365
|
+
r"#?-?\+? ?0?(\*|\d*)\.?(\d*|\*)(?:hh|h|ll|l|q|L|j|z|Z|t)?"
|
|
366
|
+
r"([diouxXfFeEgGaAcsbqQSC])")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def printf_formats(string):
|
|
370
|
+
out = []
|
|
371
|
+
i = 0
|
|
372
|
+
n = len(string)
|
|
373
|
+
while i < n:
|
|
374
|
+
c = string[i]
|
|
375
|
+
if c != "%":
|
|
376
|
+
i += 1
|
|
377
|
+
continue
|
|
378
|
+
if string[i + 1:i + 2] == "%":
|
|
379
|
+
i += 2
|
|
380
|
+
continue
|
|
381
|
+
if string[i + 1:i + 2] == "(":
|
|
382
|
+
end = string.find(")", i + 2)
|
|
383
|
+
if end == -1 or end + 1 >= n:
|
|
384
|
+
return "".join(out) if end == -1 else "".join(out)
|
|
385
|
+
out.append(string[end + 1])
|
|
386
|
+
i = end + 2
|
|
387
|
+
continue
|
|
388
|
+
m = PRINTF_FORMAT_RE.match(string, i + 1)
|
|
389
|
+
if m:
|
|
390
|
+
if m.group(1) == "*":
|
|
391
|
+
out.append("*")
|
|
392
|
+
if m.group(2) == "*":
|
|
393
|
+
out.append("*")
|
|
394
|
+
out.append(m.group(3))
|
|
395
|
+
i = m.end()
|
|
396
|
+
else:
|
|
397
|
+
if i + 1 < n:
|
|
398
|
+
out.append(string[i + 1])
|
|
399
|
+
i += 2
|
|
400
|
+
return "".join(out)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@node_check("T_SimpleCommand")
|
|
404
|
+
def check_printf_var(ctx, node):
|
|
405
|
+
if ctx.command_basename(node) != "printf":
|
|
406
|
+
return
|
|
407
|
+
args = list(ctx.argument_words(node))
|
|
408
|
+
while args:
|
|
409
|
+
lit = literal_text(args[0])
|
|
410
|
+
if lit == "--":
|
|
411
|
+
args = args[1:]
|
|
412
|
+
elif lit == "-v":
|
|
413
|
+
args = args[2:]
|
|
414
|
+
elif lit and lit.startswith("-v"):
|
|
415
|
+
args = args[1:]
|
|
416
|
+
else:
|
|
417
|
+
break
|
|
418
|
+
if not args:
|
|
419
|
+
return
|
|
420
|
+
fmt, params = args[0], args[1:]
|
|
421
|
+
lit = quoted_literal_text(fmt)
|
|
422
|
+
if lit is not None:
|
|
423
|
+
formats = printf_formats(lit)
|
|
424
|
+
fcount, acount = len(formats), len(params)
|
|
425
|
+
if acount == 0 and fcount == 0:
|
|
426
|
+
pass
|
|
427
|
+
elif fcount == 0 and acount > 0:
|
|
428
|
+
ctx.err(fmt, 2182, "This printf format string has no variables."
|
|
429
|
+
" Other arguments are ignored.")
|
|
430
|
+
elif any(may_become_multiple_args(p) or not is_constant(p)
|
|
431
|
+
and _has_glob_part(p) for p in params):
|
|
432
|
+
pass
|
|
433
|
+
elif acount < fcount and all(c == "T" for c in formats[acount:]):
|
|
434
|
+
pass
|
|
435
|
+
elif acount > 0 and acount % fcount == 0:
|
|
436
|
+
pass
|
|
437
|
+
elif any(_has_glob_part(p) for p in params):
|
|
438
|
+
pass
|
|
439
|
+
else:
|
|
440
|
+
ctx.warn(fmt, 2183, "This format string has %d %s, but is"
|
|
441
|
+
" passed %d %s."
|
|
442
|
+
% (fcount, "variable" if fcount == 1 else "variables",
|
|
443
|
+
acount,
|
|
444
|
+
"argument" if acount == 1 else "arguments"))
|
|
445
|
+
approx = word_approx(fmt)
|
|
446
|
+
if "%" not in approx and not is_constant(fmt):
|
|
447
|
+
ctx.info(fmt, 2059, "Don't use variables in the printf format"
|
|
448
|
+
" string. Use printf '..%s..' \"$foo\".")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _has_glob_part(word):
|
|
452
|
+
return any(p.kind in ("T_Glob", "T_Extglob", "T_BraceExpansion")
|
|
453
|
+
for p in word_parts(word))
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ----------------------------------------------------------------------
|
|
457
|
+
# SC2181: checking $? indirectly
|
|
458
|
+
|
|
459
|
+
@node_check("TC_Binary", "TA_Binary", "TA_Unary", "TA_Sequence")
|
|
460
|
+
def check_return_against_zero(ctx, node):
|
|
461
|
+
k = node.kind
|
|
462
|
+
|
|
463
|
+
def is_exit_code(t):
|
|
464
|
+
if t is None:
|
|
465
|
+
return False
|
|
466
|
+
if t.kind == "TA_Expansion":
|
|
467
|
+
return len(t.parts) == 1 \
|
|
468
|
+
and t.parts[0].kind == "T_DollarBraced" \
|
|
469
|
+
and t.parts[0].content == "?"
|
|
470
|
+
from ..astlib import expanded_parts
|
|
471
|
+
parts = expanded_parts(t) if t.kind == "T_NormalWord" else []
|
|
472
|
+
return len(parts) == 1 and parts[0].kind == "T_DollarBraced" \
|
|
473
|
+
and parts[0].content == "?"
|
|
474
|
+
|
|
475
|
+
def is_zero(t):
|
|
476
|
+
if t is None:
|
|
477
|
+
return False
|
|
478
|
+
if t.kind == "TA_Literal":
|
|
479
|
+
return t.value == "0"
|
|
480
|
+
return quoted_literal_text(t) == "0"
|
|
481
|
+
|
|
482
|
+
target = None
|
|
483
|
+
for_success = True
|
|
484
|
+
if k == "TC_Binary":
|
|
485
|
+
if is_zero(node.rhs) and is_exit_code(node.lhs):
|
|
486
|
+
target = node.lhs
|
|
487
|
+
for_success = node.op not in ("-gt", "-ne", "!=")
|
|
488
|
+
elif is_zero(node.lhs) and is_exit_code(node.rhs):
|
|
489
|
+
target = node.rhs
|
|
490
|
+
for_success = node.op not in ("-ne", "!=")
|
|
491
|
+
elif k == "TA_Binary":
|
|
492
|
+
if node.op in (">", "<", ">=", "<=", "==", "!="):
|
|
493
|
+
if is_zero(node.right) and is_exit_code(node.left):
|
|
494
|
+
target = node.left
|
|
495
|
+
for_success = node.op not in (">", "!=")
|
|
496
|
+
elif is_zero(node.left) and is_exit_code(node.right):
|
|
497
|
+
target = node.right
|
|
498
|
+
for_success = node.op != "!="
|
|
499
|
+
elif k == "TA_Unary":
|
|
500
|
+
if node.op == "!" and is_exit_code(node.operand):
|
|
501
|
+
target = node.operand
|
|
502
|
+
for_success = False
|
|
503
|
+
elif k == "TA_Sequence":
|
|
504
|
+
return # handled via T_Arithmetic below
|
|
505
|
+
if target is None:
|
|
506
|
+
return
|
|
507
|
+
if not _is_only_test_in_command(node):
|
|
508
|
+
return
|
|
509
|
+
if _is_first_command_in_function(ctx, node):
|
|
510
|
+
return
|
|
511
|
+
ctx.style(target, 2181, "Check exit code directly with e.g. 'if %smycmd;',"
|
|
512
|
+
" not indirectly with $?." % ("" if for_success else "! "))
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@node_check("T_Arithmetic")
|
|
516
|
+
def check_bare_exit_code(ctx, node):
|
|
517
|
+
# (( $? )) and (( ! $? ))
|
|
518
|
+
expr = node.expr
|
|
519
|
+
while expr is not None and expr.kind in ("TA_Parenthesis",):
|
|
520
|
+
expr = expr.expr
|
|
521
|
+
inverted = False
|
|
522
|
+
while expr is not None and expr.kind == "TA_Unary" and expr.op == "!":
|
|
523
|
+
inverted = not inverted
|
|
524
|
+
expr = expr.operand
|
|
525
|
+
while expr is not None and expr.kind == "TA_Parenthesis":
|
|
526
|
+
expr = expr.expr
|
|
527
|
+
if expr is not None and expr.kind == "TA_Expansion" \
|
|
528
|
+
and len(expr.parts) == 1 \
|
|
529
|
+
and expr.parts[0].kind == "T_DollarBraced" \
|
|
530
|
+
and expr.parts[0].content == "?":
|
|
531
|
+
if _is_first_command_in_function(ctx, node):
|
|
532
|
+
return
|
|
533
|
+
ctx.style(expr, 2181, "Check exit code directly with e.g."
|
|
534
|
+
" 'if %smycmd;', not indirectly with $?."
|
|
535
|
+
% ("" if inverted else "! "))
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _is_only_test_in_command(node):
|
|
539
|
+
prev = node
|
|
540
|
+
for a in ancestors(node):
|
|
541
|
+
k = a.kind
|
|
542
|
+
if k == "T_Condition":
|
|
543
|
+
return True
|
|
544
|
+
if k == "T_Arithmetic":
|
|
545
|
+
return True
|
|
546
|
+
if k in ("TC_Unary", "TA_Unary"):
|
|
547
|
+
if a.op.lstrip("|") != "!":
|
|
548
|
+
return False
|
|
549
|
+
prev = a
|
|
550
|
+
continue
|
|
551
|
+
if k in ("TC_Group", "TA_Parenthesis"):
|
|
552
|
+
prev = a
|
|
553
|
+
continue
|
|
554
|
+
if k == "TA_Sequence":
|
|
555
|
+
if len(a.exprs) != 1:
|
|
556
|
+
return False
|
|
557
|
+
prev = a
|
|
558
|
+
continue
|
|
559
|
+
return False
|
|
560
|
+
return False
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _is_first_command_in_function(ctx, node):
|
|
564
|
+
func = None
|
|
565
|
+
for a in ancestors(node):
|
|
566
|
+
if a.kind == "T_Function":
|
|
567
|
+
func = a
|
|
568
|
+
break
|
|
569
|
+
if a.kind == "T_Script":
|
|
570
|
+
return False
|
|
571
|
+
if func is None:
|
|
572
|
+
return False
|
|
573
|
+
cmd = None
|
|
574
|
+
for a in [node] + list(ancestors(node)):
|
|
575
|
+
if a.kind in ("T_Condition", "T_Arithmetic"):
|
|
576
|
+
cmd = a
|
|
577
|
+
break
|
|
578
|
+
first = _first_command_in(func.body)
|
|
579
|
+
return first is not None and cmd is not None and first is cmd
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _first_command_in(node):
|
|
583
|
+
k = node.kind
|
|
584
|
+
if k in ("T_BraceGroup", "T_Subshell"):
|
|
585
|
+
return _first_command_in(node.commands[0]) if node.commands else None
|
|
586
|
+
if k in ("T_AndIf", "T_OrIf"):
|
|
587
|
+
return _first_command_in(node.left)
|
|
588
|
+
if k == "T_Pipeline":
|
|
589
|
+
return _first_command_in(node.commands[0])
|
|
590
|
+
if k in ("T_Banged", "T_Timed"):
|
|
591
|
+
return _first_command_in(node.command)
|
|
592
|
+
if k == "T_IfExpression":
|
|
593
|
+
cond = node.branches[0][0]
|
|
594
|
+
return _first_command_in(cond[0]) if cond else node
|
|
595
|
+
return node
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# ----------------------------------------------------------------------
|
|
599
|
+
# SC2064: trap with prematurely expanded contents
|
|
600
|
+
|
|
601
|
+
@node_check("T_SimpleCommand")
|
|
602
|
+
def check_trap_quotes(ctx, node):
|
|
603
|
+
if ctx.command_basename(node) != "trap":
|
|
604
|
+
return
|
|
605
|
+
args = ctx.argument_words(node)
|
|
606
|
+
if len(args) < 2:
|
|
607
|
+
return
|
|
608
|
+
word = args[0]
|
|
609
|
+
for p in word_parts(word):
|
|
610
|
+
if p.kind == "T_DoubleQuoted":
|
|
611
|
+
for q in p.parts:
|
|
612
|
+
if q.kind in ("T_DollarBraced", "T_DollarExpansion",
|
|
613
|
+
"T_Backticked", "T_DollarArithmetic"):
|
|
614
|
+
ctx.warn(q, 2064, "Use single quotes, otherwise this"
|
|
615
|
+
" expands now rather than when signalled.")
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# ----------------------------------------------------------------------
|
|
620
|
+
# SC2065: test redirects
|
|
621
|
+
|
|
622
|
+
@node_check("T_SimpleCommand")
|
|
623
|
+
def check_test_redirects(ctx, node):
|
|
624
|
+
if ctx.command_basename(node) != "test":
|
|
625
|
+
return
|
|
626
|
+
for r in node.get("redirects", ()):
|
|
627
|
+
op = r.op
|
|
628
|
+
if isinstance(op, str) or op.kind != "T_IoFile":
|
|
629
|
+
continue
|
|
630
|
+
if r.get("fd") == "2":
|
|
631
|
+
continue
|
|
632
|
+
if op.op in (">", "<"):
|
|
633
|
+
ctx.warn(r, 2065, "This is interpreted as a shell file"
|
|
634
|
+
" redirection, not a comparison.")
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
# ----------------------------------------------------------------------
|
|
638
|
+
# SC2114 / SC2115: catastrophic rm
|
|
639
|
+
|
|
640
|
+
IMPORTANT_PATH_BASES = [
|
|
641
|
+
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
|
|
642
|
+
"/var", "/lib", "/dev", "/media", "/boot", "/lib64", "/usr/bin",
|
|
643
|
+
]
|
|
644
|
+
IMPORTANT_PATHS = frozenset(
|
|
645
|
+
base + suffix
|
|
646
|
+
for base in IMPORTANT_PATH_BASES
|
|
647
|
+
for suffix in ("", "/", "/*", "/*/*")
|
|
648
|
+
if base + suffix
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
@node_check("T_SimpleCommand")
|
|
653
|
+
def check_catastrophic_rm(ctx, node):
|
|
654
|
+
if ctx.command_basename(node) != "rm":
|
|
655
|
+
return
|
|
656
|
+
if not any(f in ("r", "R", "recursive", "f" "r")
|
|
657
|
+
for f in flags_of(ctx, node)):
|
|
658
|
+
if not any(f in ("r", "R", "recursive")
|
|
659
|
+
for f in flags_of(ctx, node)):
|
|
660
|
+
return
|
|
661
|
+
for word in ctx.argument_words(node):
|
|
662
|
+
lit = literal_text(word)
|
|
663
|
+
if lit is not None and lit.startswith("-"):
|
|
664
|
+
continue
|
|
665
|
+
for variant, is_literal in _rm_paths(word):
|
|
666
|
+
if variant is None:
|
|
667
|
+
continue
|
|
668
|
+
path = _fix_path(variant)
|
|
669
|
+
if path in IMPORTANT_PATHS:
|
|
670
|
+
if is_literal:
|
|
671
|
+
ctx.warn(word, 2114, "Warning: deletes a system"
|
|
672
|
+
" directory.")
|
|
673
|
+
else:
|
|
674
|
+
ctx.warn(word, 2115, 'Use "${var:?}" to ensure this'
|
|
675
|
+
" never expands to %s ." % path)
|
|
676
|
+
break
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _fix_path(path):
|
|
680
|
+
path = re.sub(r"/+", "/", path)
|
|
681
|
+
path = re.sub(r"\*+", "*", path)
|
|
682
|
+
if path != "/":
|
|
683
|
+
path = path.rstrip("/")
|
|
684
|
+
return path
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _rm_paths(word, depth=0):
|
|
688
|
+
"""Yield (potential_path, is_fully_literal) for each brace variant.
|
|
689
|
+
|
|
690
|
+
potential_path is None for variants containing a :?-guarded expansion.
|
|
691
|
+
"""
|
|
692
|
+
parts = word.parts if word.kind in ("T_NormalWord", "T_DoubleQuoted",
|
|
693
|
+
"T_DollarDoubleQuoted") \
|
|
694
|
+
else [word]
|
|
695
|
+
variants = [("", True)]
|
|
696
|
+
for p in parts:
|
|
697
|
+
k = p.kind
|
|
698
|
+
add = None
|
|
699
|
+
if k in ("T_Literal", "T_SingleQuoted", "T_DollarSingleQuoted"):
|
|
700
|
+
add = [(p.text, True)]
|
|
701
|
+
elif k in ("T_DoubleQuoted", "T_DollarDoubleQuoted"):
|
|
702
|
+
sub = list(_rm_paths(p, depth + 1))
|
|
703
|
+
add = sub
|
|
704
|
+
elif k == "T_Glob":
|
|
705
|
+
add = [(p.text, False)]
|
|
706
|
+
elif k == "T_DollarBraced":
|
|
707
|
+
content = p.content
|
|
708
|
+
if any(g in content for g in (":?", ":-", ":=")):
|
|
709
|
+
add = [(None, False)]
|
|
710
|
+
else:
|
|
711
|
+
add = [("", False)]
|
|
712
|
+
elif k == "T_BraceExpansion":
|
|
713
|
+
if depth > 3:
|
|
714
|
+
add = [("", False)]
|
|
715
|
+
else:
|
|
716
|
+
alts = []
|
|
717
|
+
for alt_text in _expand_brace_text(p.text):
|
|
718
|
+
from ..parser import Parser, ParseError
|
|
719
|
+
try:
|
|
720
|
+
sub = Parser(alt_text)
|
|
721
|
+
w = sub.read_word()
|
|
722
|
+
if w is not None and sub.at_end():
|
|
723
|
+
alts.extend(_rm_paths(w, depth + 1))
|
|
724
|
+
else:
|
|
725
|
+
alts.append((alt_text, True))
|
|
726
|
+
except ParseError:
|
|
727
|
+
alts.append((alt_text, True))
|
|
728
|
+
add = alts or [("", False)]
|
|
729
|
+
else:
|
|
730
|
+
add = [("", False)]
|
|
731
|
+
new = []
|
|
732
|
+
for base, base_lit in variants:
|
|
733
|
+
for text, lit in add:
|
|
734
|
+
if base is None or text is None:
|
|
735
|
+
new.append((None, False))
|
|
736
|
+
else:
|
|
737
|
+
new.append((base + text, base_lit and lit))
|
|
738
|
+
if len(new) >= 64:
|
|
739
|
+
break
|
|
740
|
+
if len(new) >= 64:
|
|
741
|
+
break
|
|
742
|
+
variants = new
|
|
743
|
+
return variants
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _expand_brace_text(text):
|
|
747
|
+
inner = text[1:-1]
|
|
748
|
+
items = []
|
|
749
|
+
depth = 0
|
|
750
|
+
start = 0
|
|
751
|
+
for i, c in enumerate(inner):
|
|
752
|
+
if c == "{":
|
|
753
|
+
depth += 1
|
|
754
|
+
elif c == "}":
|
|
755
|
+
depth -= 1
|
|
756
|
+
elif c == "," and depth == 0:
|
|
757
|
+
items.append(inner[start:i])
|
|
758
|
+
start = i + 1
|
|
759
|
+
items.append(inner[start:])
|
|
760
|
+
if len(items) == 1 and ".." in items[0]:
|
|
761
|
+
items = items[0].split("..")[:2]
|
|
762
|
+
return items
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
# ----------------------------------------------------------------------
|
|
766
|
+
# SC2174: mkdir -pm
|
|
767
|
+
|
|
768
|
+
@node_check("T_SimpleCommand")
|
|
769
|
+
def check_mkdir_dash_pm(ctx, node):
|
|
770
|
+
if ctx.command_basename(node) != "mkdir":
|
|
771
|
+
return
|
|
772
|
+
flags = ctx.flags(node)
|
|
773
|
+
names = [f for f, w in flags]
|
|
774
|
+
if not ("p" in names or "parents" in names):
|
|
775
|
+
return
|
|
776
|
+
mode_word = None
|
|
777
|
+
for f, w in flags:
|
|
778
|
+
if f in ("m", "mode"):
|
|
779
|
+
mode_word = w
|
|
780
|
+
if mode_word is None:
|
|
781
|
+
return
|
|
782
|
+
args = ctx.argument_words(node)
|
|
783
|
+
safe_re = re.compile(r"^(\.\.?/)+[^/]+$")
|
|
784
|
+
for w in args[1:]:
|
|
785
|
+
lit = literal_text(w)
|
|
786
|
+
if lit is None:
|
|
787
|
+
could = True
|
|
788
|
+
else:
|
|
789
|
+
could = "/" in lit and not safe_re.match(lit)
|
|
790
|
+
if could:
|
|
791
|
+
ctx.warn(mode_word, 2174, "When used with -p, -m only applies"
|
|
792
|
+
" to the deepest directory.")
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
# ----------------------------------------------------------------------
|
|
797
|
+
# SC2188 / SC2189: redirection without a command
|
|
798
|
+
|
|
799
|
+
@node_check("T_SimpleCommand")
|
|
800
|
+
def check_redirected_nowhere(ctx, node):
|
|
801
|
+
if node.words or node.assigns:
|
|
802
|
+
return
|
|
803
|
+
redirects = node.get("redirects", ())
|
|
804
|
+
if not redirects:
|
|
805
|
+
return
|
|
806
|
+
parent = node.parent
|
|
807
|
+
# var=$(< file) idiom
|
|
808
|
+
if parent is not None and parent.kind in ("T_DollarExpansion",
|
|
809
|
+
"T_Backticked") \
|
|
810
|
+
and len(parent.commands) == 1:
|
|
811
|
+
if all(not isinstance(r.op, str) and r.op.kind == "T_IoFile"
|
|
812
|
+
and r.op.op == "<" for r in redirects):
|
|
813
|
+
return
|
|
814
|
+
in_pipeline = parent is not None and parent.kind == "T_Pipeline"
|
|
815
|
+
if in_pipeline:
|
|
816
|
+
ctx.err(node, 2189, "You can't have | between this redirection"
|
|
817
|
+
" and the command it should apply to.")
|
|
818
|
+
else:
|
|
819
|
+
ctx.warn(node, 2188, "This redirection doesn't have a command."
|
|
820
|
+
" Move to its command (or use 'true' as no-op).")
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
# ----------------------------------------------------------------------
|
|
824
|
+
# SC2148 (+2187, 2239, 2246): shebang
|
|
825
|
+
|
|
826
|
+
@tree_check
|
|
827
|
+
def check_shebang(ctx, root):
|
|
828
|
+
shebang = root.get("shebang")
|
|
829
|
+
has_shell_directive = any(d.kind == "shell" for d in ctx.directives)
|
|
830
|
+
if shebang is None or not shebang.startswith("#!"):
|
|
831
|
+
if not has_shell_directive and ctx.explicit_shell is None:
|
|
832
|
+
f_pos = 0
|
|
833
|
+
ctx.report(root, 2148, "error",
|
|
834
|
+
"Tips depend on target shell and yours is unknown."
|
|
835
|
+
" Add a shebang or a 'shell' directive.",
|
|
836
|
+
pos=f_pos, end=min(1, len(ctx.source)))
|
|
837
|
+
return
|
|
838
|
+
m = re.match(r"#!\s*(\S+)(\s+(\S+))?", shebang)
|
|
839
|
+
if not m:
|
|
840
|
+
return
|
|
841
|
+
interpreter = m.group(1)
|
|
842
|
+
basename = interpreter.rsplit("/", 1)[-1]
|
|
843
|
+
if basename == "env" and m.group(3):
|
|
844
|
+
basename = m.group(3).rsplit("/", 1)[-1]
|
|
845
|
+
elif basename == "busybox" and m.group(3):
|
|
846
|
+
return # busybox sh/ash handled as ash without warnings
|
|
847
|
+
if interpreter.endswith("/"):
|
|
848
|
+
ctx.report(root, 2246, "error",
|
|
849
|
+
"This shebang specifies a directory. Ensure the"
|
|
850
|
+
" interpreter is a file.",
|
|
851
|
+
pos=0, end=len(shebang))
|
|
852
|
+
return
|
|
853
|
+
if not interpreter.startswith("/") and basename == interpreter \
|
|
854
|
+
not in ("env",):
|
|
855
|
+
if "/" in interpreter and not has_shell_directive:
|
|
856
|
+
pass
|
|
857
|
+
if not interpreter.startswith("/") and "/" in interpreter \
|
|
858
|
+
and not has_shell_directive:
|
|
859
|
+
ctx.report(root, 2239, "error",
|
|
860
|
+
"Ensure the shebang uses an absolute path to the"
|
|
861
|
+
" interpreter.", pos=0, end=len(shebang))
|
|
862
|
+
if basename == "ash" and not has_shell_directive:
|
|
863
|
+
ctx.report(root, 2187, "info",
|
|
864
|
+
"Ash scripts will be checked as Dash. Add"
|
|
865
|
+
" '# shellcheck shell=dash' to silence.",
|
|
866
|
+
pos=0, end=len(shebang))
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
# ----------------------------------------------------------------------
|
|
870
|
+
# SC2003: expr is antiquated
|
|
871
|
+
|
|
872
|
+
EXPR_EXCEPTIONS = frozenset({":", "<", ">", "<=", ">=",
|
|
873
|
+
"match", "length", "substr", "index"})
|
|
874
|
+
|
|
875
|
+
EXPR_OP_MSG = {
|
|
876
|
+
"match": "'expr match' has unspecified results. Prefer"
|
|
877
|
+
" 'expr str : regex'.",
|
|
878
|
+
"length": "'expr length' has unspecified results. Prefer ${#var}.",
|
|
879
|
+
"substr": "'expr substr' has unspecified results. Prefer 'cut' or"
|
|
880
|
+
" ${var#???}.",
|
|
881
|
+
"index": "'expr index' has unspecified results. Prefer"
|
|
882
|
+
" x=${var%%[chars]*}; $((${#x}+1)).",
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@node_check("T_SimpleCommand")
|
|
887
|
+
def check_expr(ctx, node):
|
|
888
|
+
if ctx.command_basename(node) != "expr":
|
|
889
|
+
return
|
|
890
|
+
args = list(ctx.argument_words(node))
|
|
891
|
+
lits = [quoted_literal_text(w) for w in args]
|
|
892
|
+
raw_lits = [literal_text(w) for w in args]
|
|
893
|
+
|
|
894
|
+
if all(lit is None or lit not in EXPR_EXCEPTIONS for lit in raw_lits):
|
|
895
|
+
ctx.style(node, 2003, "expr is antiquated. Consider rewriting this"
|
|
896
|
+
" using $((..)), ${} or [[ ]].")
|
|
897
|
+
|
|
898
|
+
def check_op(word):
|
|
899
|
+
lit = literal_text(word)
|
|
900
|
+
if lit in EXPR_OP_MSG:
|
|
901
|
+
ctx.warn(word, 2308, EXPR_OP_MSG[lit])
|
|
902
|
+
|
|
903
|
+
if len(args) == 3:
|
|
904
|
+
lhs, op, rhs = args
|
|
905
|
+
check_op(lhs)
|
|
906
|
+
op_parts = word_parts(op)
|
|
907
|
+
if len(op_parts) == 1 and op_parts[0].kind == "T_Glob" \
|
|
908
|
+
and op_parts[0].text == "*":
|
|
909
|
+
ctx.err(op, 2304, "* must be escaped to multiply: \\*."
|
|
910
|
+
" Modern $((x * y)) avoids this issue.")
|
|
911
|
+
elif literal_text(op) == ":" and _has_glob_part(rhs):
|
|
912
|
+
ctx.warn(rhs, 2305, "Quote regex argument to expr to avoid"
|
|
913
|
+
" it expanding as a glob.")
|
|
914
|
+
elif len(args) == 1:
|
|
915
|
+
if not will_split(args[0]) \
|
|
916
|
+
and not may_become_multiple_args(args[0]):
|
|
917
|
+
ctx.warn(args[0], 2307, "'expr' expects 3+ arguments but sees"
|
|
918
|
+
" 1. Make sure each operator/operand is a separate"
|
|
919
|
+
" argument, and escape <>&|.")
|
|
920
|
+
elif len(args) == 2:
|
|
921
|
+
if raw_lits[0] != "length" and not will_split(args[0]) \
|
|
922
|
+
and not will_split(args[1]) \
|
|
923
|
+
and not any(may_become_multiple_args(a) for a in args):
|
|
924
|
+
check_op(args[0])
|
|
925
|
+
ctx.warn(node, 2307, "'expr' expects 3+ arguments, but sees 2."
|
|
926
|
+
" Make sure each operator/operand is a separate"
|
|
927
|
+
" argument, and escape <>&|.")
|
|
928
|
+
else:
|
|
929
|
+
check_op(args[0])
|
|
930
|
+
for w in args[1:]:
|
|
931
|
+
if _has_glob_part(w):
|
|
932
|
+
ctx.warn(w, 2306, "Escape glob characters in arguments"
|
|
933
|
+
" to expr to avoid pathname expansion.")
|
|
934
|
+
elif args:
|
|
935
|
+
check_op(args[0])
|
|
936
|
+
for w in args[1:]:
|
|
937
|
+
if _has_glob_part(w):
|
|
938
|
+
ctx.warn(w, 2306, "Escape glob characters in arguments"
|
|
939
|
+
" to expr to avoid pathname expansion.")
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
# ----------------------------------------------------------------------
|
|
943
|
+
# SC2015: shorthand if
|
|
944
|
+
|
|
945
|
+
@node_check("T_OrIf")
|
|
946
|
+
def check_shorthand_if(ctx, node):
|
|
947
|
+
left = node.left
|
|
948
|
+
if left.kind != "T_AndIf":
|
|
949
|
+
return
|
|
950
|
+
# A && B || C
|
|
951
|
+
if _is_ok_shorthand(node.right) or is_condition(node):
|
|
952
|
+
return
|
|
953
|
+
if _is_test_like(left.right):
|
|
954
|
+
return
|
|
955
|
+
ctx.info(node, 2015, "Note that A && B || C is not if-then-else."
|
|
956
|
+
" C may run when A is true.")
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def _is_test_like(node):
|
|
960
|
+
k = node.kind
|
|
961
|
+
if k in ("T_Condition", "T_Arithmetic"):
|
|
962
|
+
return True
|
|
963
|
+
if k in ("T_Banged", "T_Timed"):
|
|
964
|
+
return _is_test_like(node.command)
|
|
965
|
+
if k == "T_SimpleCommand":
|
|
966
|
+
return first_word_basename(node) == "test"
|
|
967
|
+
return False
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _is_ok_shorthand(node):
|
|
971
|
+
if node.kind == "T_SimpleCommand":
|
|
972
|
+
if not node.words and node.assigns:
|
|
973
|
+
return True
|
|
974
|
+
name = first_word_basename(node)
|
|
975
|
+
return name in ("echo", "exit", "return", "printf", "true", ":")
|
|
976
|
+
return False
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
# ----------------------------------------------------------------------
|
|
980
|
+
# SC2050 / SC2193: constant conditions
|
|
981
|
+
|
|
982
|
+
ARITH_TEST_OPS = frozenset({"-eq", "-ne", "-lt", "-le", "-gt", "-ge"})
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
@node_check("TC_Binary")
|
|
986
|
+
def check_constant_conditions(ctx, node):
|
|
987
|
+
cond = None
|
|
988
|
+
for a in ancestors(node):
|
|
989
|
+
if a.kind == "T_Condition":
|
|
990
|
+
cond = a
|
|
991
|
+
break
|
|
992
|
+
if a.kind in ("T_SimpleCommand", "T_Script"):
|
|
993
|
+
break
|
|
994
|
+
if cond is None:
|
|
995
|
+
return
|
|
996
|
+
# in [[ ]], arithmetic comparisons evaluate names as variables
|
|
997
|
+
if node.op in ARITH_TEST_OPS and not cond.single:
|
|
998
|
+
return
|
|
999
|
+
if node.op in ("-nt", "-ot", "-ef"):
|
|
1000
|
+
return
|
|
1001
|
+
if is_constant(node.lhs) and is_constant(node.rhs):
|
|
1002
|
+
ctx.warn(node, 2050, "This expression is constant. Did you forget"
|
|
1003
|
+
" the $ on a variable?")
|