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.
@@ -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?")