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
pureshellcheck/shast.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""AST node model for shell scripts.
|
|
2
|
+
|
|
3
|
+
Node kinds mirror ShellCheck's AST constructors (T_SimpleCommand,
|
|
4
|
+
T_DollarBraced, ...) so that check logic ports naturally. Nodes are generic
|
|
5
|
+
objects with a `kind` tag plus per-kind fields stored in a dict and exposed
|
|
6
|
+
as attributes.
|
|
7
|
+
|
|
8
|
+
Positions `pos`/`end` are absolute character offsets into the source; use
|
|
9
|
+
`Positions` to translate to 1-based (line, column).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import bisect
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Node:
|
|
16
|
+
__slots__ = ("kind", "pos", "end", "parent", "fields")
|
|
17
|
+
|
|
18
|
+
def __init__(self, kind, pos, end, **fields):
|
|
19
|
+
self.kind = kind
|
|
20
|
+
self.pos = pos
|
|
21
|
+
self.end = end
|
|
22
|
+
self.parent = None
|
|
23
|
+
self.fields = fields
|
|
24
|
+
|
|
25
|
+
def __getattr__(self, name):
|
|
26
|
+
try:
|
|
27
|
+
return self.fields[name]
|
|
28
|
+
except KeyError:
|
|
29
|
+
raise AttributeError("%s node has no field %r" % (self.kind, name))
|
|
30
|
+
|
|
31
|
+
def get(self, name, default=None):
|
|
32
|
+
return self.fields.get(name, default)
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
inner = ", ".join(
|
|
36
|
+
"%s=%r" % (k, v) for k, v in self.fields.items() if k != "parent"
|
|
37
|
+
)
|
|
38
|
+
return "%s(%s)" % (self.kind, inner)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Children traversal: every node field that may hold Node or [Node] or
|
|
42
|
+
# [[Node]] is walked generically.
|
|
43
|
+
|
|
44
|
+
def iter_children(node):
|
|
45
|
+
for value in node.fields.values():
|
|
46
|
+
if isinstance(value, Node):
|
|
47
|
+
yield value
|
|
48
|
+
elif isinstance(value, list):
|
|
49
|
+
for item in value:
|
|
50
|
+
if isinstance(item, Node):
|
|
51
|
+
yield item
|
|
52
|
+
elif isinstance(item, list):
|
|
53
|
+
for sub in item:
|
|
54
|
+
if isinstance(sub, Node):
|
|
55
|
+
yield sub
|
|
56
|
+
elif isinstance(item, tuple):
|
|
57
|
+
for sub in item:
|
|
58
|
+
if isinstance(sub, Node):
|
|
59
|
+
yield sub
|
|
60
|
+
elif isinstance(sub, list):
|
|
61
|
+
for s2 in sub:
|
|
62
|
+
if isinstance(s2, Node):
|
|
63
|
+
yield s2
|
|
64
|
+
elif isinstance(value, tuple):
|
|
65
|
+
for item in value:
|
|
66
|
+
if isinstance(item, Node):
|
|
67
|
+
yield item
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def walk(node):
|
|
71
|
+
"""Yield node and all descendants in document order."""
|
|
72
|
+
stack = [node]
|
|
73
|
+
while stack:
|
|
74
|
+
n = stack.pop()
|
|
75
|
+
yield n
|
|
76
|
+
children = list(iter_children(n))
|
|
77
|
+
children.reverse()
|
|
78
|
+
stack.extend(children)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def set_parents(root):
|
|
82
|
+
for n in walk(root):
|
|
83
|
+
for c in iter_children(n):
|
|
84
|
+
c.parent = n
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ancestors(node):
|
|
88
|
+
n = node.parent
|
|
89
|
+
while n is not None:
|
|
90
|
+
yield n
|
|
91
|
+
n = n.parent
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Positions:
|
|
95
|
+
"""Translate absolute offsets to 1-based (line, col)."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, source):
|
|
98
|
+
self.line_starts = [0]
|
|
99
|
+
idx = source.find("\n")
|
|
100
|
+
while idx != -1:
|
|
101
|
+
self.line_starts.append(idx + 1)
|
|
102
|
+
idx = source.find("\n", idx + 1)
|
|
103
|
+
|
|
104
|
+
def line_col(self, offset):
|
|
105
|
+
line = bisect.bisect_right(self.line_starts, offset)
|
|
106
|
+
return line, offset - self.line_starts[line - 1] + 1
|
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
"""Execution-order variable flow engine.
|
|
2
|
+
|
|
3
|
+
Walks the AST in rough execution order, tracking each variable's "space
|
|
4
|
+
status" (EMPTY / CLEAN / DIRTY) and integer attribute, calling a callback at
|
|
5
|
+
every parameter expansion with the state at that point. This is a pragmatic
|
|
6
|
+
reimplementation of the value tracking ShellCheck performs on its CFG; it
|
|
7
|
+
handles straight-line code, branches (worst-case merge), loops (single pass),
|
|
8
|
+
subshell isolation, function definitions/calls with dynamic scoping and
|
|
9
|
+
`local`, and branch-exit pruning (`if x; then v='a b'; exit; fi`).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .astlib import (
|
|
13
|
+
CLEAN, DIRTY, EMPTY, SPECIAL_INTEGER_VARIABLES,
|
|
14
|
+
VARIABLES_WITHOUT_SPACES, braced_reference, join_status,
|
|
15
|
+
literal_space_status, merge_status,
|
|
16
|
+
)
|
|
17
|
+
from .parser import literal_text
|
|
18
|
+
from .shast import walk
|
|
19
|
+
|
|
20
|
+
EXIT_COMMANDS = {"exit", "return"}
|
|
21
|
+
|
|
22
|
+
DECLARING_COMMANDS = {"declare", "typeset", "local", "export", "readonly"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VarInfo:
|
|
26
|
+
__slots__ = ("status", "integer")
|
|
27
|
+
|
|
28
|
+
def __init__(self, status, integer=False):
|
|
29
|
+
self.status = status
|
|
30
|
+
self.integer = integer
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Scope:
|
|
34
|
+
"""One level of variable scope (global or function-local)."""
|
|
35
|
+
|
|
36
|
+
__slots__ = ("vars",)
|
|
37
|
+
|
|
38
|
+
def __init__(self, vars=None):
|
|
39
|
+
self.vars = vars if vars is not None else {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FuncDef:
|
|
43
|
+
__slots__ = ("node", "conditional", "walked")
|
|
44
|
+
|
|
45
|
+
def __init__(self, node, conditional):
|
|
46
|
+
self.node = node
|
|
47
|
+
self.conditional = conditional
|
|
48
|
+
self.walked = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class VarFlow:
|
|
52
|
+
"""on_reference(braced_node, name, status, integer) is called for every
|
|
53
|
+
T_DollarBraced in execution order."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, on_reference=None, shell="bash", on_assign=None):
|
|
56
|
+
self.on_reference = on_reference or (lambda *a: None)
|
|
57
|
+
self.on_assign = on_assign or (lambda *a: None)
|
|
58
|
+
self.shell = shell
|
|
59
|
+
self.scopes = [Scope()]
|
|
60
|
+
self.functions = {}
|
|
61
|
+
self.call_stack = []
|
|
62
|
+
self.conditional_depth = 0
|
|
63
|
+
|
|
64
|
+
# -- scope management ------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def lookup(self, name):
|
|
67
|
+
for scope in reversed(self.scopes):
|
|
68
|
+
info = scope.vars.get(name)
|
|
69
|
+
if info is not None:
|
|
70
|
+
return info
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def assign(self, name, status, integer=None, local=False, global_=False):
|
|
74
|
+
if global_:
|
|
75
|
+
scope = self.scopes[0]
|
|
76
|
+
elif local:
|
|
77
|
+
scope = self.scopes[-1]
|
|
78
|
+
else:
|
|
79
|
+
scope = self.scopes[0]
|
|
80
|
+
for s in reversed(self.scopes):
|
|
81
|
+
if name in s.vars:
|
|
82
|
+
scope = s
|
|
83
|
+
break
|
|
84
|
+
old = scope.vars.get(name)
|
|
85
|
+
if integer is None:
|
|
86
|
+
integer = old.integer if old is not None else False
|
|
87
|
+
elif self.conditional_depth:
|
|
88
|
+
# attribute only maybe applied: keep the weaker assumption
|
|
89
|
+
integer = integer and (old.integer if old is not None else False)
|
|
90
|
+
if integer and status == DIRTY:
|
|
91
|
+
status = CLEAN
|
|
92
|
+
if self.conditional_depth and old is not None:
|
|
93
|
+
status = merge_status(old.status, status)
|
|
94
|
+
elif self.conditional_depth and old is None:
|
|
95
|
+
status = DIRTY
|
|
96
|
+
scope.vars[name] = VarInfo(status, integer)
|
|
97
|
+
|
|
98
|
+
def snapshot(self):
|
|
99
|
+
return [dict((k, VarInfo(v.status, v.integer))
|
|
100
|
+
for k, v in s.vars.items()) for s in self.scopes]
|
|
101
|
+
|
|
102
|
+
def restore(self, snap):
|
|
103
|
+
for scope, vars_ in zip(self.scopes, snap):
|
|
104
|
+
scope.vars = vars_
|
|
105
|
+
|
|
106
|
+
def merge_snapshots(self, snaps):
|
|
107
|
+
"""Merge variable states from multiple branches (worst case)."""
|
|
108
|
+
merged = []
|
|
109
|
+
for level in range(len(self.scopes)):
|
|
110
|
+
allnames = set()
|
|
111
|
+
for snap in snaps:
|
|
112
|
+
allnames.update(snap[level])
|
|
113
|
+
vars_ = {}
|
|
114
|
+
for name in allnames:
|
|
115
|
+
infos = [snap[level].get(name) for snap in snaps]
|
|
116
|
+
status = None
|
|
117
|
+
integer = True
|
|
118
|
+
for info in infos:
|
|
119
|
+
s = info.status if info is not None else DIRTY
|
|
120
|
+
i = info.integer if info is not None else False
|
|
121
|
+
status = s if status is None else merge_status(status, s)
|
|
122
|
+
integer = integer and i
|
|
123
|
+
vars_[name] = VarInfo(status, integer)
|
|
124
|
+
merged.append(vars_)
|
|
125
|
+
self.restore(merged)
|
|
126
|
+
|
|
127
|
+
# -- value evaluation --------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def ref_status(self, name):
|
|
130
|
+
if name in SPECIAL_INTEGER_VARIABLES:
|
|
131
|
+
return CLEAN, True
|
|
132
|
+
if name in VARIABLES_WITHOUT_SPACES:
|
|
133
|
+
return CLEAN, False
|
|
134
|
+
if name in ("@", "*") or name.isdigit():
|
|
135
|
+
return DIRTY, False
|
|
136
|
+
info = self.lookup(name)
|
|
137
|
+
if info is None:
|
|
138
|
+
return DIRTY, False
|
|
139
|
+
if info.integer:
|
|
140
|
+
return CLEAN, True
|
|
141
|
+
return info.status, False
|
|
142
|
+
|
|
143
|
+
def word_status(self, word):
|
|
144
|
+
"""SpaceStatus of a word's value (assignment RHS semantics)."""
|
|
145
|
+
if word is None:
|
|
146
|
+
return EMPTY
|
|
147
|
+
total = EMPTY
|
|
148
|
+
for part in self._value_parts(word):
|
|
149
|
+
total = join_status(total, self._part_status(part))
|
|
150
|
+
return total
|
|
151
|
+
|
|
152
|
+
def _value_parts(self, word):
|
|
153
|
+
if word.kind == "T_NormalWord":
|
|
154
|
+
return word.parts
|
|
155
|
+
return [word]
|
|
156
|
+
|
|
157
|
+
def _part_status(self, part):
|
|
158
|
+
k = part.kind
|
|
159
|
+
if k == "T_Literal":
|
|
160
|
+
return literal_space_status(part.text)
|
|
161
|
+
if k in ("T_SingleQuoted", "T_DollarSingleQuoted"):
|
|
162
|
+
return literal_space_status(part.text) if part.text else EMPTY
|
|
163
|
+
if k in ("T_DoubleQuoted", "T_DollarDoubleQuoted"):
|
|
164
|
+
total = EMPTY
|
|
165
|
+
for q in part.parts:
|
|
166
|
+
total = join_status(total, self._part_status(q))
|
|
167
|
+
return total
|
|
168
|
+
if k == "T_DollarBraced":
|
|
169
|
+
content = part.content
|
|
170
|
+
name = braced_reference(content)
|
|
171
|
+
if content.startswith("#"):
|
|
172
|
+
return CLEAN
|
|
173
|
+
status, _ = self.ref_status(name)
|
|
174
|
+
return status
|
|
175
|
+
if k in ("T_DollarExpansion", "T_Backticked",
|
|
176
|
+
"T_DollarBraceCommandExpansion"):
|
|
177
|
+
return DIRTY
|
|
178
|
+
if k == "T_DollarArithmetic":
|
|
179
|
+
return CLEAN
|
|
180
|
+
if k in ("T_Glob", "T_Extglob", "T_BraceExpansion"):
|
|
181
|
+
return DIRTY
|
|
182
|
+
if k == "T_ProcSub":
|
|
183
|
+
return CLEAN
|
|
184
|
+
return DIRTY
|
|
185
|
+
|
|
186
|
+
# -- main walk ---------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def run(self, root):
|
|
189
|
+
self.process_statements(root.commands)
|
|
190
|
+
# walk function bodies that were never called, with the final state
|
|
191
|
+
for name, fd in sorted(self.functions.items()):
|
|
192
|
+
if not fd.walked:
|
|
193
|
+
self.call_function(fd, simulate_only=True)
|
|
194
|
+
|
|
195
|
+
def process_statements(self, statements):
|
|
196
|
+
"""Returns True if execution definitely exits within the block."""
|
|
197
|
+
for i, stmt in enumerate(statements):
|
|
198
|
+
if self.process(stmt):
|
|
199
|
+
return True
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def process(self, node):
|
|
203
|
+
"""Process one statement; returns True if it definitely exits."""
|
|
204
|
+
k = node.kind
|
|
205
|
+
method = getattr(self, "_do_" + k, None)
|
|
206
|
+
if method is not None:
|
|
207
|
+
return method(node)
|
|
208
|
+
# generic: walk children as statements/expressions
|
|
209
|
+
self.visit_word(node)
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
# each _do_ method returns exits:bool
|
|
213
|
+
|
|
214
|
+
def _do_T_SimpleCommand(self, node):
|
|
215
|
+
cmd_name = None
|
|
216
|
+
if node.words:
|
|
217
|
+
cmd_name = literal_text(node.words[0])
|
|
218
|
+
if cmd_name:
|
|
219
|
+
cmd_name = cmd_name.rsplit("/", 1)[-1]
|
|
220
|
+
is_declaring = cmd_name in DECLARING_COMMANDS
|
|
221
|
+
flags = ""
|
|
222
|
+
unflags = ""
|
|
223
|
+
if is_declaring:
|
|
224
|
+
for w in node.words[1:]:
|
|
225
|
+
t = literal_text(w)
|
|
226
|
+
if t and t.startswith("-"):
|
|
227
|
+
flags += t[1:]
|
|
228
|
+
elif t and t.startswith("+"):
|
|
229
|
+
unflags += t[1:]
|
|
230
|
+
in_function = len(self.scopes) > 1
|
|
231
|
+
is_local = (cmd_name in ("local", "declare", "typeset")
|
|
232
|
+
and in_function and "g" not in flags)
|
|
233
|
+
integer = True if "i" in flags else (False if "i" in unflags
|
|
234
|
+
else None)
|
|
235
|
+
|
|
236
|
+
for assign in node.assigns:
|
|
237
|
+
self.visit_word(assign.get("value"))
|
|
238
|
+
for idx in assign.get("indices", ()):
|
|
239
|
+
self.visit_word(idx)
|
|
240
|
+
value = assign.get("value")
|
|
241
|
+
if value is not None and value.kind == "T_Array":
|
|
242
|
+
status = EMPTY
|
|
243
|
+
for el in value.elements:
|
|
244
|
+
v = el.value if el.kind == "T_IndexedElement" else el
|
|
245
|
+
status = merge_status(status, self.word_status(v)) \
|
|
246
|
+
if status != EMPTY else self.word_status(v)
|
|
247
|
+
else:
|
|
248
|
+
status = self.word_status(value)
|
|
249
|
+
if assign.get("append"):
|
|
250
|
+
old = self.lookup(assign.name)
|
|
251
|
+
if old is not None:
|
|
252
|
+
status = join_status(old.status, status)
|
|
253
|
+
self.assign(assign.name, status, integer=integer,
|
|
254
|
+
local=is_local, global_="g" in flags)
|
|
255
|
+
self.on_assign(assign.name, value, assign)
|
|
256
|
+
|
|
257
|
+
for w in node.words:
|
|
258
|
+
self.visit_word(w)
|
|
259
|
+
for r in node.get("redirects", ()):
|
|
260
|
+
self.visit_word(r)
|
|
261
|
+
self._apply_redirect_assign(r)
|
|
262
|
+
|
|
263
|
+
if is_declaring:
|
|
264
|
+
# bare names: declare -x NAME / local NAME
|
|
265
|
+
for w in node.words[1:]:
|
|
266
|
+
t = literal_text(w)
|
|
267
|
+
if t and not t.startswith("-") and not t.startswith("+") \
|
|
268
|
+
and "=" not in t and t.isidentifier():
|
|
269
|
+
if is_local:
|
|
270
|
+
self.assign(t, EMPTY, integer=integer, local=True)
|
|
271
|
+
elif cmd_name in ("export", "readonly") or "x" in flags:
|
|
272
|
+
self.assign(t, DIRTY, integer=integer)
|
|
273
|
+
elif integer is not None:
|
|
274
|
+
old = self.lookup(t)
|
|
275
|
+
self.assign(t, old.status if old else EMPTY,
|
|
276
|
+
integer=integer)
|
|
277
|
+
elif cmd_name == "read":
|
|
278
|
+
self._apply_read(node)
|
|
279
|
+
elif cmd_name in ("mapfile", "readarray"):
|
|
280
|
+
args = [literal_text(w) for w in node.words[1:]]
|
|
281
|
+
names = [a for a in args if a and not a.startswith("-")]
|
|
282
|
+
if names:
|
|
283
|
+
self.assign(names[-1], DIRTY)
|
|
284
|
+
elif cmd_name == "getopts":
|
|
285
|
+
args = [w for w in node.words[1:]]
|
|
286
|
+
if len(args) >= 2:
|
|
287
|
+
t = literal_text(args[1])
|
|
288
|
+
if t:
|
|
289
|
+
self.assign(t, CLEAN)
|
|
290
|
+
elif cmd_name == "wait":
|
|
291
|
+
args = [literal_text(w) for w in node.words[1:]]
|
|
292
|
+
for i, a in enumerate(args):
|
|
293
|
+
if a == "-p" and i + 1 < len(args) and args[i + 1]:
|
|
294
|
+
self.assign(args[i + 1], CLEAN, integer=True)
|
|
295
|
+
elif cmd_name == "printf":
|
|
296
|
+
args = [literal_text(w) for w in node.words[1:]]
|
|
297
|
+
for i, a in enumerate(args):
|
|
298
|
+
if a == "-v" and i + 1 < len(args) and args[i + 1]:
|
|
299
|
+
self.assign(args[i + 1], DIRTY)
|
|
300
|
+
elif cmd_name == "unset":
|
|
301
|
+
for w in node.words[1:]:
|
|
302
|
+
t = literal_text(w)
|
|
303
|
+
if t and not t.startswith("-"):
|
|
304
|
+
self.assign(t.split("[", 1)[0], DIRTY)
|
|
305
|
+
elif cmd_name in EXIT_COMMANDS:
|
|
306
|
+
return True
|
|
307
|
+
elif cmd_name == "exec" and len(node.words) > 1:
|
|
308
|
+
return True
|
|
309
|
+
elif cmd_name in self.functions:
|
|
310
|
+
self.call_function(self.functions[cmd_name])
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
def _apply_read(self, node):
|
|
314
|
+
names = []
|
|
315
|
+
args = node.words[1:]
|
|
316
|
+
skip_next = False
|
|
317
|
+
for i, w in enumerate(args):
|
|
318
|
+
t = literal_text(w)
|
|
319
|
+
if skip_next:
|
|
320
|
+
skip_next = False
|
|
321
|
+
if t:
|
|
322
|
+
names.append(t)
|
|
323
|
+
continue
|
|
324
|
+
if t and t.startswith("-"):
|
|
325
|
+
for opt in ("a", "d", "i", "n", "N", "p", "t", "u"):
|
|
326
|
+
if t.endswith(opt):
|
|
327
|
+
skip_next = opt == "a"
|
|
328
|
+
break
|
|
329
|
+
if t.endswith("-a"):
|
|
330
|
+
skip_next = True
|
|
331
|
+
continue
|
|
332
|
+
if t:
|
|
333
|
+
names.append(t)
|
|
334
|
+
if not names:
|
|
335
|
+
names = ["REPLY"]
|
|
336
|
+
for n in names:
|
|
337
|
+
self.assign(n.split("[", 1)[0], DIRTY)
|
|
338
|
+
|
|
339
|
+
def _apply_redirect_assign(self, redirect):
|
|
340
|
+
fd = redirect.get("fd")
|
|
341
|
+
if fd and fd.startswith("{") and fd.endswith("}"):
|
|
342
|
+
self.assign(fd[1:-1], CLEAN, integer=True)
|
|
343
|
+
|
|
344
|
+
def _do_T_Pipeline(self, node):
|
|
345
|
+
for cmd in node.commands:
|
|
346
|
+
snap = self.snapshot()
|
|
347
|
+
self.process(cmd)
|
|
348
|
+
self.restore(snap)
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
def _do_T_Banged(self, node):
|
|
352
|
+
return self.process(node.command)
|
|
353
|
+
|
|
354
|
+
def _do_T_Timed(self, node):
|
|
355
|
+
return self.process(node.command)
|
|
356
|
+
|
|
357
|
+
def _do_T_Backgrounded(self, node):
|
|
358
|
+
snap = self.snapshot()
|
|
359
|
+
self.process(node.command)
|
|
360
|
+
self.restore(snap)
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
def _do_T_AndIf(self, node):
|
|
364
|
+
self.process(node.left)
|
|
365
|
+
self.conditional_depth += 1
|
|
366
|
+
try:
|
|
367
|
+
self.process(node.right)
|
|
368
|
+
finally:
|
|
369
|
+
self.conditional_depth -= 1
|
|
370
|
+
return False
|
|
371
|
+
|
|
372
|
+
_do_T_OrIf = _do_T_AndIf
|
|
373
|
+
|
|
374
|
+
def _do_T_IfExpression(self, node):
|
|
375
|
+
snaps = []
|
|
376
|
+
for cond, body in node.branches:
|
|
377
|
+
self.process_statements(cond)
|
|
378
|
+
snap_before = self.snapshot()
|
|
379
|
+
exited = self.process_statements(body)
|
|
380
|
+
if not exited:
|
|
381
|
+
snaps.append(self.snapshot())
|
|
382
|
+
self.restore(snap_before)
|
|
383
|
+
else_body = node.else_body
|
|
384
|
+
if else_body:
|
|
385
|
+
snap_before = self.snapshot()
|
|
386
|
+
exited = self.process_statements(else_body)
|
|
387
|
+
if not exited:
|
|
388
|
+
snaps.append(self.snapshot())
|
|
389
|
+
self.restore(snap_before)
|
|
390
|
+
else:
|
|
391
|
+
snaps.append(self.snapshot())
|
|
392
|
+
if snaps:
|
|
393
|
+
self.merge_snapshots(snaps)
|
|
394
|
+
return False
|
|
395
|
+
return True # all branches exited and else missing? keep current
|
|
396
|
+
|
|
397
|
+
def _do_T_WhileExpression(self, node):
|
|
398
|
+
self.process_statements(node.condition)
|
|
399
|
+
before = self.snapshot()
|
|
400
|
+
self.process_statements(node.body)
|
|
401
|
+
self.merge_snapshots([before, self.snapshot()])
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
_do_T_UntilExpression = _do_T_WhileExpression
|
|
405
|
+
|
|
406
|
+
def _do_T_ForIn(self, node):
|
|
407
|
+
status = None
|
|
408
|
+
for w in node.words:
|
|
409
|
+
self.visit_word(w)
|
|
410
|
+
s = self.word_status(w)
|
|
411
|
+
status = s if status is None else merge_status(status, s)
|
|
412
|
+
if not node.has_in or status is None:
|
|
413
|
+
status = DIRTY # implicit "$@"
|
|
414
|
+
self.assign(node.variable, status)
|
|
415
|
+
before = self.snapshot()
|
|
416
|
+
self.process_statements(node.body)
|
|
417
|
+
self.merge_snapshots([before, self.snapshot()])
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
_do_T_SelectIn = _do_T_ForIn
|
|
421
|
+
|
|
422
|
+
def _do_T_ForArithmetic(self, node):
|
|
423
|
+
self.visit_arith(node.init)
|
|
424
|
+
self.visit_arith(node.condition)
|
|
425
|
+
before = self.snapshot()
|
|
426
|
+
self.process_statements(node.body)
|
|
427
|
+
self.visit_arith(node.update)
|
|
428
|
+
self.merge_snapshots([before, self.snapshot()])
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
def _do_T_CaseExpression(self, node):
|
|
432
|
+
self.visit_word(node.word)
|
|
433
|
+
snaps = [self.snapshot()]
|
|
434
|
+
for item in node.items:
|
|
435
|
+
for p in item.patterns:
|
|
436
|
+
self.visit_word(p)
|
|
437
|
+
before = self.snapshot()
|
|
438
|
+
exited = self.process_statements(item.body)
|
|
439
|
+
if not exited:
|
|
440
|
+
snaps.append(self.snapshot())
|
|
441
|
+
self.restore(before)
|
|
442
|
+
self.merge_snapshots(snaps)
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
def _do_T_BraceGroup(self, node):
|
|
446
|
+
return self.process_statements(node.commands)
|
|
447
|
+
|
|
448
|
+
def _do_T_Subshell(self, node):
|
|
449
|
+
snap = self.snapshot()
|
|
450
|
+
self.process_statements(node.commands)
|
|
451
|
+
self.restore(snap)
|
|
452
|
+
return False
|
|
453
|
+
|
|
454
|
+
def _do_T_Condition(self, node):
|
|
455
|
+
self.visit_word(node.expr)
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
def _do_T_Arithmetic(self, node):
|
|
459
|
+
self.visit_arith(node.expr)
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
def _do_T_Function(self, node):
|
|
463
|
+
conditional = self.conditional_depth > 0
|
|
464
|
+
self.functions[node.name] = FuncDef(node, conditional)
|
|
465
|
+
return False
|
|
466
|
+
|
|
467
|
+
def _do_T_BatsTest(self, node):
|
|
468
|
+
self.assign("status", CLEAN, integer=True)
|
|
469
|
+
self.assign("output", DIRTY)
|
|
470
|
+
self.assign("lines", DIRTY)
|
|
471
|
+
self.assign("stderr", DIRTY)
|
|
472
|
+
snap = self.snapshot()
|
|
473
|
+
self.scopes.append(Scope())
|
|
474
|
+
try:
|
|
475
|
+
self.process_statements(node.body.commands)
|
|
476
|
+
finally:
|
|
477
|
+
self.scopes.pop()
|
|
478
|
+
self.restore(snap)
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
def _do_T_CoProc(self, node):
|
|
482
|
+
snap = self.snapshot()
|
|
483
|
+
self.process(node.command)
|
|
484
|
+
self.restore(snap)
|
|
485
|
+
return False
|
|
486
|
+
|
|
487
|
+
def call_function(self, fd, simulate_only=False):
|
|
488
|
+
if fd.node in [f for f in self.call_stack]:
|
|
489
|
+
return
|
|
490
|
+
fd.walked = True
|
|
491
|
+
self.call_stack.append(fd.node)
|
|
492
|
+
snap = self.snapshot() if (fd.conditional or simulate_only) else None
|
|
493
|
+
self.scopes.append(Scope())
|
|
494
|
+
try:
|
|
495
|
+
body = fd.node.body
|
|
496
|
+
if body.kind == "T_BraceGroup":
|
|
497
|
+
self.process_statements(body.commands)
|
|
498
|
+
else:
|
|
499
|
+
self.process(body)
|
|
500
|
+
finally:
|
|
501
|
+
self.scopes.pop()
|
|
502
|
+
self.call_stack.pop()
|
|
503
|
+
if snap is not None:
|
|
504
|
+
if fd.conditional and not simulate_only:
|
|
505
|
+
self.merge_snapshots([snap, self.snapshot()])
|
|
506
|
+
else:
|
|
507
|
+
self.restore(snap)
|
|
508
|
+
|
|
509
|
+
# -- expression-level walking ------------------------------------------
|
|
510
|
+
|
|
511
|
+
def visit_word(self, node):
|
|
512
|
+
"""Walk any non-statement subtree, processing references and nested
|
|
513
|
+
command substitutions."""
|
|
514
|
+
if node is None or isinstance(node, str):
|
|
515
|
+
return
|
|
516
|
+
k = node.kind
|
|
517
|
+
if k == "T_DollarBraced":
|
|
518
|
+
name = braced_reference(node.content)
|
|
519
|
+
status, integer = self.ref_status(name)
|
|
520
|
+
self.on_reference(node, name, status, integer)
|
|
521
|
+
for p in node.get("arg_parts", ()):
|
|
522
|
+
self.visit_word(p)
|
|
523
|
+
for idx in node.get("indices", ()):
|
|
524
|
+
self.visit_word(idx)
|
|
525
|
+
return
|
|
526
|
+
if k in ("T_DollarExpansion", "T_Backticked", "T_ProcSub",
|
|
527
|
+
"T_DollarBraceCommandExpansion"):
|
|
528
|
+
snap = self.snapshot()
|
|
529
|
+
self.process_statements(node.commands)
|
|
530
|
+
self.restore(snap)
|
|
531
|
+
return
|
|
532
|
+
if k in ("T_DollarArithmetic",):
|
|
533
|
+
self.visit_arith(node.expr)
|
|
534
|
+
return
|
|
535
|
+
if k in ("TA_Sequence", "TA_Assignment", "TA_Binary", "TA_Unary",
|
|
536
|
+
"TA_Trinary", "TA_Variable", "TA_Parenthesis",
|
|
537
|
+
"TA_Expansion", "TA_Literal", "TA_Empty"):
|
|
538
|
+
self.visit_arith(node)
|
|
539
|
+
return
|
|
540
|
+
if k == "T_FdRedirect":
|
|
541
|
+
op = node.op
|
|
542
|
+
if isinstance(op, str):
|
|
543
|
+
return
|
|
544
|
+
self.visit_word(op)
|
|
545
|
+
self._apply_redirect_assign(node)
|
|
546
|
+
return
|
|
547
|
+
from .shast import iter_children
|
|
548
|
+
for c in iter_children(node):
|
|
549
|
+
self.visit_word(c)
|
|
550
|
+
|
|
551
|
+
def visit_arith(self, node):
|
|
552
|
+
if node is None:
|
|
553
|
+
return
|
|
554
|
+
k = node.kind
|
|
555
|
+
if k == "TA_Variable":
|
|
556
|
+
for idx in node.get("indices", ()):
|
|
557
|
+
self.visit_word(idx)
|
|
558
|
+
return
|
|
559
|
+
if k == "TA_Assignment":
|
|
560
|
+
self.visit_arith(node.right)
|
|
561
|
+
left = node.left
|
|
562
|
+
if left.kind == "TA_Variable":
|
|
563
|
+
self.assign(left.name, CLEAN)
|
|
564
|
+
else:
|
|
565
|
+
self.visit_arith(left)
|
|
566
|
+
return
|
|
567
|
+
if k == "TA_Unary":
|
|
568
|
+
op = node.op.lstrip("|")
|
|
569
|
+
operand = node.operand
|
|
570
|
+
if op in ("++", "--") and operand.kind == "TA_Variable":
|
|
571
|
+
self.assign(operand.name, CLEAN)
|
|
572
|
+
else:
|
|
573
|
+
self.visit_arith(operand)
|
|
574
|
+
return
|
|
575
|
+
if k == "TA_Expansion":
|
|
576
|
+
for p in node.parts:
|
|
577
|
+
self.visit_word(p)
|
|
578
|
+
return
|
|
579
|
+
from .shast import iter_children
|
|
580
|
+
for c in iter_children(node):
|
|
581
|
+
if c.kind.startswith("TA_"):
|
|
582
|
+
self.visit_arith(c)
|
|
583
|
+
else:
|
|
584
|
+
self.visit_word(c)
|