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/parser.py
ADDED
|
@@ -0,0 +1,2050 @@
|
|
|
1
|
+
"""Recursive-descent parser for bash scripts.
|
|
2
|
+
|
|
3
|
+
Produces the Node AST defined in shast.py. The parser is deliberately
|
|
4
|
+
lenient: it aims to parse anything bash would accept (plus a few common
|
|
5
|
+
near-miss constructs) so that analysis can run on real-world scripts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from .shast import Node
|
|
11
|
+
|
|
12
|
+
KEYWORDS = {
|
|
13
|
+
"if", "then", "elif", "else", "fi", "while", "until", "for", "do",
|
|
14
|
+
"done", "case", "esac", "select", "in", "function", "{", "}", "!",
|
|
15
|
+
"[[", "]]", "time", "coproc",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
END_KEYWORDS = {
|
|
19
|
+
"then", "elif", "else", "fi", "do", "done", "esac", "}", "]]", "in",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Characters that terminate an unquoted word.
|
|
23
|
+
METACHARS = " \t\n;&|<>()"
|
|
24
|
+
|
|
25
|
+
UNQUOTED_RUN = re.compile(r"""[^\\'"$`*?\[\](){}<>|&;\s!#~]+""")
|
|
26
|
+
DQUOTE_RUN = re.compile(r'[^"\\$`]+')
|
|
27
|
+
SQUOTE_END = re.compile(r"'")
|
|
28
|
+
NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
29
|
+
ASSIGN_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)(\[|\+?=)")
|
|
30
|
+
DIGITS_RE = re.compile(r"[0-9]+")
|
|
31
|
+
SPECIAL_PARAMS = set("@*#?$!-0123456789_")
|
|
32
|
+
HEREDOC_LINE = re.compile(r".*\n|.+$")
|
|
33
|
+
|
|
34
|
+
BINARY_TEST_OPS = {
|
|
35
|
+
"==", "!=", "=", "=~", "<", ">", "\\<", "\\>", "<=", ">=",
|
|
36
|
+
"-eq", "-ne", "-lt", "-le", "-gt", "-ge", "-nt", "-ot", "-ef",
|
|
37
|
+
}
|
|
38
|
+
UNARY_TEST_OPS = {
|
|
39
|
+
"-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-k", "-n", "-o",
|
|
40
|
+
"-p", "-r", "-s", "-t", "-u", "-v", "-w", "-x", "-z", "-G", "-L",
|
|
41
|
+
"-N", "-O", "-R", "-S",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ARITH_ASSIGN_OPS = (
|
|
45
|
+
"<<=", ">>=", "+=", "-=", "*=", "/=", "%=", "&=", "^=", "|=", "=",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class QuotedOp(str):
|
|
50
|
+
"""A test operator that appeared quoted, e.g. '>' in [ $a '>' $b ]."""
|
|
51
|
+
|
|
52
|
+
def __new__(cls, text, width):
|
|
53
|
+
self = str.__new__(cls, text)
|
|
54
|
+
self.width = width
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def quoted_literal_text(word):
|
|
59
|
+
"""Literal text of a word allowing quoted parts; None if it expands."""
|
|
60
|
+
if word is None or word.kind != "T_NormalWord":
|
|
61
|
+
return None
|
|
62
|
+
out = []
|
|
63
|
+
for p in word.parts:
|
|
64
|
+
if p.kind in ("T_Literal", "T_SingleQuoted", "T_DollarSingleQuoted"):
|
|
65
|
+
out.append(p.text)
|
|
66
|
+
elif p.kind == "T_DoubleQuoted":
|
|
67
|
+
for q in p.parts:
|
|
68
|
+
if q.kind != "T_Literal":
|
|
69
|
+
return None
|
|
70
|
+
out.append(q.text)
|
|
71
|
+
else:
|
|
72
|
+
return None
|
|
73
|
+
return "".join(out)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ParseError(Exception):
|
|
77
|
+
def __init__(self, message, pos, code=1073):
|
|
78
|
+
super().__init__(message)
|
|
79
|
+
self.message = message
|
|
80
|
+
self.pos = pos
|
|
81
|
+
self.code = code
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Directive:
|
|
85
|
+
"""A `# shellcheck key=value` comment."""
|
|
86
|
+
|
|
87
|
+
__slots__ = ("pos", "line_has_code", "kind", "values")
|
|
88
|
+
|
|
89
|
+
def __init__(self, pos, line_has_code, kind, values):
|
|
90
|
+
self.pos = pos
|
|
91
|
+
self.line_has_code = line_has_code
|
|
92
|
+
self.kind = kind
|
|
93
|
+
self.values = values
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
DIRECTIVE_RE = re.compile(r"^#\s*shellcheck\s+(.*)$")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Parser:
|
|
100
|
+
def __init__(self, source):
|
|
101
|
+
self.src = source
|
|
102
|
+
self.n = len(source)
|
|
103
|
+
self.i = 0
|
|
104
|
+
self.pending_heredocs = []
|
|
105
|
+
self.directives = []
|
|
106
|
+
self.line_had_command = False
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
# Low-level helpers
|
|
110
|
+
|
|
111
|
+
def peek(self, offset=0):
|
|
112
|
+
i = self.i + offset
|
|
113
|
+
return self.src[i] if i < self.n else ""
|
|
114
|
+
|
|
115
|
+
def at_end(self):
|
|
116
|
+
return self.i >= self.n
|
|
117
|
+
|
|
118
|
+
def error(self, message, pos=None):
|
|
119
|
+
raise ParseError(message, self.i if pos is None else pos)
|
|
120
|
+
|
|
121
|
+
def skip_inline_ws(self):
|
|
122
|
+
src, n = self.src, self.n
|
|
123
|
+
i = self.i
|
|
124
|
+
while i < n:
|
|
125
|
+
c = src[i]
|
|
126
|
+
if c == " " or c == "\t" or c == "\r":
|
|
127
|
+
i += 1
|
|
128
|
+
elif c == "\\" and i + 1 < n and src[i + 1] == "\n":
|
|
129
|
+
i += 2
|
|
130
|
+
else:
|
|
131
|
+
break
|
|
132
|
+
self.i = i
|
|
133
|
+
|
|
134
|
+
def consume_comment(self):
|
|
135
|
+
"""Consume a comment starting at '#'. Records directives."""
|
|
136
|
+
start = self.i
|
|
137
|
+
end = self.src.find("\n", start)
|
|
138
|
+
if end == -1:
|
|
139
|
+
end = self.n
|
|
140
|
+
text = self.src[start:end]
|
|
141
|
+
self.i = end
|
|
142
|
+
m = DIRECTIVE_RE.match(text)
|
|
143
|
+
if m:
|
|
144
|
+
for field in m.group(1).split():
|
|
145
|
+
if "=" in field:
|
|
146
|
+
kind, _, value = field.partition("=")
|
|
147
|
+
if kind in ("disable", "enable", "shell", "source",
|
|
148
|
+
"source-path", "external-sources"):
|
|
149
|
+
self.directives.append(Directive(
|
|
150
|
+
start, self.line_had_command, kind,
|
|
151
|
+
value.split(",")))
|
|
152
|
+
|
|
153
|
+
def consume_newline(self):
|
|
154
|
+
"""Consume a newline character, then any pending heredoc bodies."""
|
|
155
|
+
assert self.peek() == "\n"
|
|
156
|
+
self.i += 1
|
|
157
|
+
self.line_had_command = False
|
|
158
|
+
if self.pending_heredocs:
|
|
159
|
+
self.read_heredoc_bodies()
|
|
160
|
+
|
|
161
|
+
def skip_ws_and_newlines(self):
|
|
162
|
+
while True:
|
|
163
|
+
self.skip_inline_ws()
|
|
164
|
+
c = self.peek()
|
|
165
|
+
if c == "#":
|
|
166
|
+
self.consume_comment()
|
|
167
|
+
elif c == "\n":
|
|
168
|
+
self.consume_newline()
|
|
169
|
+
else:
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
def peek_literal_word(self):
|
|
173
|
+
"""Return the purely-literal word starting at i, or "".
|
|
174
|
+
|
|
175
|
+
Used for keyword detection at command position; keywords are always
|
|
176
|
+
unquoted literals.
|
|
177
|
+
"""
|
|
178
|
+
src, n = self.src, self.n
|
|
179
|
+
i = self.i
|
|
180
|
+
j = i
|
|
181
|
+
while j < n and src[j] not in " \t\r\n;&|<>()'\"\\$`#":
|
|
182
|
+
j += 1
|
|
183
|
+
return src[i:j]
|
|
184
|
+
|
|
185
|
+
def at_keyword(self, kw):
|
|
186
|
+
word = self.peek_literal_word()
|
|
187
|
+
if word != kw:
|
|
188
|
+
return False
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
# ------------------------------------------------------------------
|
|
192
|
+
# Script / lists
|
|
193
|
+
|
|
194
|
+
def parse(self):
|
|
195
|
+
shebang = None
|
|
196
|
+
if self.src.startswith("#!"):
|
|
197
|
+
end = self.src.find("\n")
|
|
198
|
+
if end == -1:
|
|
199
|
+
end = self.n
|
|
200
|
+
shebang = self.src[:end]
|
|
201
|
+
self.i = end
|
|
202
|
+
commands = self.read_list(set())
|
|
203
|
+
self.skip_ws_and_newlines()
|
|
204
|
+
if not self.at_end():
|
|
205
|
+
self.error("unexpected token %r" % self.peek())
|
|
206
|
+
node = Node("T_Script", 0, self.n, shebang=shebang, commands=commands)
|
|
207
|
+
return node
|
|
208
|
+
|
|
209
|
+
def read_list(self, end_keywords, stop_case_seps=False):
|
|
210
|
+
"""Read commands until EOF, a closing token, or an end keyword."""
|
|
211
|
+
commands = []
|
|
212
|
+
while True:
|
|
213
|
+
self.skip_ws_and_newlines()
|
|
214
|
+
if self.at_end():
|
|
215
|
+
break
|
|
216
|
+
c = self.peek()
|
|
217
|
+
if c == ")":
|
|
218
|
+
break
|
|
219
|
+
if stop_case_seps and c == ";" and self.peek(1) == ";":
|
|
220
|
+
break
|
|
221
|
+
if c == ";" and self.peek(1) == "&":
|
|
222
|
+
break
|
|
223
|
+
word = self.peek_literal_word()
|
|
224
|
+
if word in END_KEYWORDS and word in end_keywords:
|
|
225
|
+
break
|
|
226
|
+
if word in END_KEYWORDS and word != "in":
|
|
227
|
+
# Unexpected closer ('}' etc.): stop and let caller decide.
|
|
228
|
+
break
|
|
229
|
+
cmd = self.read_and_or()
|
|
230
|
+
if cmd is None:
|
|
231
|
+
break
|
|
232
|
+
sep = self.read_separator()
|
|
233
|
+
if sep == "&":
|
|
234
|
+
cmd = Node("T_Backgrounded", cmd.pos, self.i, command=cmd)
|
|
235
|
+
commands.append(cmd)
|
|
236
|
+
if sep is None:
|
|
237
|
+
break
|
|
238
|
+
return commands
|
|
239
|
+
|
|
240
|
+
def read_separator(self):
|
|
241
|
+
self.skip_inline_ws()
|
|
242
|
+
if self.peek() == "#":
|
|
243
|
+
self.consume_comment()
|
|
244
|
+
c = self.peek()
|
|
245
|
+
if c == ";":
|
|
246
|
+
if self.peek(1) == ";" or self.peek(1) == "&":
|
|
247
|
+
return None # case separator, not consumed here
|
|
248
|
+
self.i += 1
|
|
249
|
+
return ";"
|
|
250
|
+
if c == "&":
|
|
251
|
+
if self.peek(1) in (">", "&"):
|
|
252
|
+
return None
|
|
253
|
+
self.i += 1
|
|
254
|
+
return "&"
|
|
255
|
+
if c == "\n":
|
|
256
|
+
self.consume_newline()
|
|
257
|
+
return "\n"
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
def read_and_or(self):
|
|
261
|
+
left = self.read_pipeline()
|
|
262
|
+
if left is None:
|
|
263
|
+
return None
|
|
264
|
+
while True:
|
|
265
|
+
self.skip_inline_ws()
|
|
266
|
+
two = self.src[self.i:self.i + 2]
|
|
267
|
+
if two == "&&":
|
|
268
|
+
kind = "T_AndIf"
|
|
269
|
+
elif two == "||":
|
|
270
|
+
kind = "T_OrIf"
|
|
271
|
+
else:
|
|
272
|
+
break
|
|
273
|
+
self.i += 2
|
|
274
|
+
self.skip_ws_and_newlines()
|
|
275
|
+
right = self.read_pipeline()
|
|
276
|
+
if right is None:
|
|
277
|
+
self.error("expected a command after %s" % two)
|
|
278
|
+
left = Node(kind, left.pos, right.end, left=left, right=right)
|
|
279
|
+
return left
|
|
280
|
+
|
|
281
|
+
def read_pipeline(self):
|
|
282
|
+
self.skip_inline_ws()
|
|
283
|
+
start = self.i
|
|
284
|
+
banged = False
|
|
285
|
+
timed = False
|
|
286
|
+
while True:
|
|
287
|
+
word = self.peek_literal_word()
|
|
288
|
+
if word == "!" and not banged:
|
|
289
|
+
banged = True
|
|
290
|
+
self.i += 1
|
|
291
|
+
self.skip_inline_ws()
|
|
292
|
+
elif word == "time":
|
|
293
|
+
timed = True
|
|
294
|
+
self.i += len(word)
|
|
295
|
+
self.skip_inline_ws()
|
|
296
|
+
# consume time flags like -p
|
|
297
|
+
while self.peek() == "-":
|
|
298
|
+
w = self.peek_literal_word()
|
|
299
|
+
if not w.startswith("-"):
|
|
300
|
+
break
|
|
301
|
+
self.i += len(w)
|
|
302
|
+
self.skip_inline_ws()
|
|
303
|
+
else:
|
|
304
|
+
break
|
|
305
|
+
cmd = self.read_command()
|
|
306
|
+
if cmd is None:
|
|
307
|
+
if banged or timed:
|
|
308
|
+
# `time` or `!` with no command
|
|
309
|
+
return Node("T_SimpleCommand", start, self.i,
|
|
310
|
+
assigns=[], words=[], redirects=[])
|
|
311
|
+
return None
|
|
312
|
+
cmds = [cmd]
|
|
313
|
+
seps = []
|
|
314
|
+
while True:
|
|
315
|
+
self.skip_inline_ws()
|
|
316
|
+
c = self.peek()
|
|
317
|
+
if c == "|" and self.peek(1) != "|":
|
|
318
|
+
op = "|&" if self.peek(1) == "&" else "|"
|
|
319
|
+
self.i += len(op)
|
|
320
|
+
seps.append(op)
|
|
321
|
+
self.skip_ws_and_newlines()
|
|
322
|
+
nxt = self.read_command()
|
|
323
|
+
if nxt is None:
|
|
324
|
+
self.error("expected a command after |")
|
|
325
|
+
cmds.append(nxt)
|
|
326
|
+
else:
|
|
327
|
+
break
|
|
328
|
+
if len(cmds) > 1:
|
|
329
|
+
result = Node("T_Pipeline", cmds[0].pos, cmds[-1].end,
|
|
330
|
+
commands=cmds, separators=seps)
|
|
331
|
+
else:
|
|
332
|
+
result = cmds[0]
|
|
333
|
+
if timed:
|
|
334
|
+
result = Node("T_Timed", start, result.end, command=result)
|
|
335
|
+
if banged:
|
|
336
|
+
result = Node("T_Banged", start, result.end, command=result)
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
# ------------------------------------------------------------------
|
|
340
|
+
# Commands
|
|
341
|
+
|
|
342
|
+
def read_command(self):
|
|
343
|
+
self.skip_inline_ws()
|
|
344
|
+
if self.at_end():
|
|
345
|
+
return None
|
|
346
|
+
c = self.peek()
|
|
347
|
+
if c == "#":
|
|
348
|
+
return None
|
|
349
|
+
if c == "(":
|
|
350
|
+
if self.peek(1) == "(":
|
|
351
|
+
node = self.read_arithmetic_command()
|
|
352
|
+
else:
|
|
353
|
+
node = self.read_subshell()
|
|
354
|
+
return self.attach_redirects(node)
|
|
355
|
+
word = self.peek_literal_word()
|
|
356
|
+
if word == "[[":
|
|
357
|
+
node = self.read_condition(False)
|
|
358
|
+
return self.attach_redirects(node)
|
|
359
|
+
if word == "[":
|
|
360
|
+
save = self.i
|
|
361
|
+
heredocs = list(self.pending_heredocs)
|
|
362
|
+
try:
|
|
363
|
+
node = self.read_condition(True)
|
|
364
|
+
return self.attach_redirects(node)
|
|
365
|
+
except ParseError:
|
|
366
|
+
# not valid test syntax; fall back to a plain command
|
|
367
|
+
self.i = save
|
|
368
|
+
self.pending_heredocs = heredocs
|
|
369
|
+
return self.read_simple_command()
|
|
370
|
+
if word == "{":
|
|
371
|
+
nxt = self.peek(1)
|
|
372
|
+
if nxt in " \t\n(":
|
|
373
|
+
node = self.read_brace_group()
|
|
374
|
+
return self.attach_redirects(node)
|
|
375
|
+
if word == "@test":
|
|
376
|
+
return self.read_bats_test()
|
|
377
|
+
if word in ("if", "while", "until", "for", "case", "select",
|
|
378
|
+
"function", "coproc"):
|
|
379
|
+
method = {
|
|
380
|
+
"if": self.read_if,
|
|
381
|
+
"while": self.read_while,
|
|
382
|
+
"until": self.read_until,
|
|
383
|
+
"for": self.read_for,
|
|
384
|
+
"case": self.read_case,
|
|
385
|
+
"select": self.read_select,
|
|
386
|
+
"function": self.read_function_kw,
|
|
387
|
+
"coproc": self.read_coproc,
|
|
388
|
+
}[word]
|
|
389
|
+
node = method()
|
|
390
|
+
return self.attach_redirects(node)
|
|
391
|
+
return self.read_simple_command()
|
|
392
|
+
|
|
393
|
+
def expect_keyword(self, kw):
|
|
394
|
+
self.skip_ws_and_newlines()
|
|
395
|
+
word = self.peek_literal_word()
|
|
396
|
+
if word != kw:
|
|
397
|
+
self.error("expected %r, found %r" % (kw, word or self.peek()))
|
|
398
|
+
self.i += len(kw)
|
|
399
|
+
|
|
400
|
+
def attach_redirects(self, node):
|
|
401
|
+
redirects = node.get("redirects")
|
|
402
|
+
if redirects is None:
|
|
403
|
+
redirects = []
|
|
404
|
+
node.fields["redirects"] = redirects
|
|
405
|
+
while True:
|
|
406
|
+
self.skip_inline_ws()
|
|
407
|
+
r = self.maybe_read_redirect()
|
|
408
|
+
if r is None:
|
|
409
|
+
break
|
|
410
|
+
redirects.append(r)
|
|
411
|
+
node.end = self.i
|
|
412
|
+
return node
|
|
413
|
+
|
|
414
|
+
def read_subshell(self):
|
|
415
|
+
start = self.i
|
|
416
|
+
self.i += 1 # (
|
|
417
|
+
cmds = self.read_list(set())
|
|
418
|
+
self.skip_ws_and_newlines()
|
|
419
|
+
if self.peek() != ")":
|
|
420
|
+
self.error("expected ) to close subshell")
|
|
421
|
+
self.i += 1
|
|
422
|
+
return Node("T_Subshell", start, self.i, commands=cmds)
|
|
423
|
+
|
|
424
|
+
def read_brace_group(self):
|
|
425
|
+
start = self.i
|
|
426
|
+
self.i += 1 # {
|
|
427
|
+
cmds = self.read_list({"}"})
|
|
428
|
+
self.skip_ws_and_newlines()
|
|
429
|
+
if self.peek_literal_word() != "}":
|
|
430
|
+
self.error("expected } to close brace group")
|
|
431
|
+
self.i += 1
|
|
432
|
+
return Node("T_BraceGroup", start, self.i, commands=cmds)
|
|
433
|
+
|
|
434
|
+
def read_arithmetic_command(self):
|
|
435
|
+
start = self.i
|
|
436
|
+
save = self.i
|
|
437
|
+
self.i += 2 # ((
|
|
438
|
+
try:
|
|
439
|
+
expr = self.read_arith_seq()
|
|
440
|
+
self.skip_inline_ws()
|
|
441
|
+
if self.src[self.i:self.i + 2] != "))":
|
|
442
|
+
self.error("expected ))")
|
|
443
|
+
self.i += 2
|
|
444
|
+
return Node("T_Arithmetic", start, self.i, expr=expr)
|
|
445
|
+
except ParseError:
|
|
446
|
+
self.i = save
|
|
447
|
+
return self.read_subshell()
|
|
448
|
+
|
|
449
|
+
def read_if(self):
|
|
450
|
+
start = self.i
|
|
451
|
+
self.i += 2 # if
|
|
452
|
+
branches = []
|
|
453
|
+
cond = self.read_list({"then"})
|
|
454
|
+
self.expect_keyword("then")
|
|
455
|
+
body = self.read_list({"elif", "else", "fi"})
|
|
456
|
+
branches.append((cond, body))
|
|
457
|
+
else_body = []
|
|
458
|
+
while True:
|
|
459
|
+
self.skip_ws_and_newlines()
|
|
460
|
+
word = self.peek_literal_word()
|
|
461
|
+
if word == "elif":
|
|
462
|
+
self.i += 4
|
|
463
|
+
cond = self.read_list({"then"})
|
|
464
|
+
self.expect_keyword("then")
|
|
465
|
+
body = self.read_list({"elif", "else", "fi"})
|
|
466
|
+
branches.append((cond, body))
|
|
467
|
+
elif word == "else":
|
|
468
|
+
self.i += 4
|
|
469
|
+
else_body = self.read_list({"fi"})
|
|
470
|
+
elif word == "fi":
|
|
471
|
+
self.i += 2
|
|
472
|
+
break
|
|
473
|
+
else:
|
|
474
|
+
self.error("expected fi, found %r"
|
|
475
|
+
% (word or self.peek()))
|
|
476
|
+
return Node("T_IfExpression", start, self.i,
|
|
477
|
+
branches=branches, else_body=else_body)
|
|
478
|
+
|
|
479
|
+
def read_do_group(self):
|
|
480
|
+
self.skip_ws_and_newlines()
|
|
481
|
+
word = self.peek_literal_word()
|
|
482
|
+
if word == "do":
|
|
483
|
+
self.i += 2
|
|
484
|
+
body = self.read_list({"done"})
|
|
485
|
+
self.expect_keyword("done")
|
|
486
|
+
return body
|
|
487
|
+
if word == "{":
|
|
488
|
+
# Not POSIX but accepted by some shells for for-loops; be lenient.
|
|
489
|
+
group = self.read_brace_group()
|
|
490
|
+
return group.commands
|
|
491
|
+
self.error("expected 'do', found %r" % (word or self.peek()))
|
|
492
|
+
|
|
493
|
+
def read_while(self):
|
|
494
|
+
start = self.i
|
|
495
|
+
self.i += 5
|
|
496
|
+
cond = self.read_list({"do"})
|
|
497
|
+
body = self.read_do_group()
|
|
498
|
+
return Node("T_WhileExpression", start, self.i,
|
|
499
|
+
condition=cond, body=body)
|
|
500
|
+
|
|
501
|
+
def read_until(self):
|
|
502
|
+
start = self.i
|
|
503
|
+
self.i += 5
|
|
504
|
+
cond = self.read_list({"do"})
|
|
505
|
+
body = self.read_do_group()
|
|
506
|
+
return Node("T_UntilExpression", start, self.i,
|
|
507
|
+
condition=cond, body=body)
|
|
508
|
+
|
|
509
|
+
def read_for(self):
|
|
510
|
+
start = self.i
|
|
511
|
+
self.i += 3
|
|
512
|
+
self.skip_inline_ws()
|
|
513
|
+
if self.peek() == "(" and self.peek(1) == "(":
|
|
514
|
+
self.i += 2
|
|
515
|
+
init = self.read_arith_seq(allow_empty=True)
|
|
516
|
+
self.expect_char(";")
|
|
517
|
+
cond = self.read_arith_seq(allow_empty=True)
|
|
518
|
+
self.expect_char(";")
|
|
519
|
+
update = self.read_arith_seq(allow_empty=True)
|
|
520
|
+
self.skip_inline_ws()
|
|
521
|
+
if self.src[self.i:self.i + 2] != "))":
|
|
522
|
+
self.error("expected )) in arithmetic for loop")
|
|
523
|
+
self.i += 2
|
|
524
|
+
self.skip_inline_ws()
|
|
525
|
+
if self.peek() == ";":
|
|
526
|
+
self.i += 1
|
|
527
|
+
body = self.read_do_group()
|
|
528
|
+
return Node("T_ForArithmetic", start, self.i, init=init,
|
|
529
|
+
condition=cond, update=update, body=body)
|
|
530
|
+
return self.read_for_in("T_ForIn", start)
|
|
531
|
+
|
|
532
|
+
def read_select(self):
|
|
533
|
+
start = self.i
|
|
534
|
+
self.i += 6
|
|
535
|
+
return self.read_for_in("T_SelectIn", start)
|
|
536
|
+
|
|
537
|
+
def read_for_in(self, kind, start):
|
|
538
|
+
self.skip_inline_ws()
|
|
539
|
+
m = NAME_RE.match(self.src, self.i)
|
|
540
|
+
if not m:
|
|
541
|
+
self.error("expected a variable name in for loop")
|
|
542
|
+
name = m.group(0)
|
|
543
|
+
name_pos = self.i
|
|
544
|
+
self.i = m.end()
|
|
545
|
+
self.skip_ws_and_newlines()
|
|
546
|
+
words = []
|
|
547
|
+
has_in = False
|
|
548
|
+
if self.peek_literal_word() == "in":
|
|
549
|
+
has_in = True
|
|
550
|
+
self.i += 2
|
|
551
|
+
while True:
|
|
552
|
+
self.skip_inline_ws()
|
|
553
|
+
c = self.peek()
|
|
554
|
+
if c in ("", ";", "\n", "#"):
|
|
555
|
+
break
|
|
556
|
+
word = self.read_word()
|
|
557
|
+
if word is None:
|
|
558
|
+
break
|
|
559
|
+
words.append(word)
|
|
560
|
+
self.skip_inline_ws()
|
|
561
|
+
if self.peek() == ";":
|
|
562
|
+
self.i += 1
|
|
563
|
+
body = self.read_do_group()
|
|
564
|
+
return Node(kind, start, self.i, variable=name, var_pos=name_pos,
|
|
565
|
+
has_in=has_in, words=words, body=body)
|
|
566
|
+
|
|
567
|
+
def read_case(self):
|
|
568
|
+
start = self.i
|
|
569
|
+
self.i += 4
|
|
570
|
+
self.skip_inline_ws()
|
|
571
|
+
word = self.read_word()
|
|
572
|
+
if word is None:
|
|
573
|
+
self.error("expected a word after 'case'")
|
|
574
|
+
self.expect_keyword("in")
|
|
575
|
+
items = []
|
|
576
|
+
while True:
|
|
577
|
+
self.skip_ws_and_newlines()
|
|
578
|
+
if self.peek_literal_word() == "esac":
|
|
579
|
+
self.i += 4
|
|
580
|
+
break
|
|
581
|
+
if self.at_end():
|
|
582
|
+
self.error("expected esac")
|
|
583
|
+
if self.peek() == "(":
|
|
584
|
+
self.i += 1
|
|
585
|
+
self.skip_inline_ws()
|
|
586
|
+
patterns = []
|
|
587
|
+
while True:
|
|
588
|
+
pat = self.read_word()
|
|
589
|
+
if pat is None:
|
|
590
|
+
self.error("expected a case pattern")
|
|
591
|
+
patterns.append(pat)
|
|
592
|
+
self.skip_inline_ws()
|
|
593
|
+
if self.peek() == "|":
|
|
594
|
+
self.i += 1
|
|
595
|
+
self.skip_inline_ws()
|
|
596
|
+
else:
|
|
597
|
+
break
|
|
598
|
+
if self.peek() != ")":
|
|
599
|
+
self.error("expected ) after case pattern")
|
|
600
|
+
self.i += 1
|
|
601
|
+
body = self.read_list({"esac"}, stop_case_seps=True)
|
|
602
|
+
self.skip_ws_and_newlines()
|
|
603
|
+
sep = ";;"
|
|
604
|
+
two = self.src[self.i:self.i + 2]
|
|
605
|
+
if two == ";;":
|
|
606
|
+
if self.peek(2) == "&":
|
|
607
|
+
sep = ";;&"
|
|
608
|
+
self.i += 3
|
|
609
|
+
else:
|
|
610
|
+
self.i += 2
|
|
611
|
+
elif two == ";&":
|
|
612
|
+
sep = ";&"
|
|
613
|
+
self.i += 2
|
|
614
|
+
items.append(Node("T_CaseItem", patterns[0].pos, self.i,
|
|
615
|
+
patterns=patterns, body=body, separator=sep))
|
|
616
|
+
return Node("T_CaseExpression", start, self.i, word=word, items=items)
|
|
617
|
+
|
|
618
|
+
def read_function_kw(self):
|
|
619
|
+
start = self.i
|
|
620
|
+
self.i += 8 # function
|
|
621
|
+
self.skip_inline_ws()
|
|
622
|
+
j = self.i
|
|
623
|
+
while j < self.n and self.src[j] not in " \t\n;&|<>()'\"`$\\":
|
|
624
|
+
j += 1
|
|
625
|
+
name = self.src[self.i:j]
|
|
626
|
+
if not name:
|
|
627
|
+
self.error("expected a function name")
|
|
628
|
+
self.i = j
|
|
629
|
+
self.skip_inline_ws()
|
|
630
|
+
if self.peek() == "(" and self.peek(1) == ")":
|
|
631
|
+
self.i += 2
|
|
632
|
+
return self.finish_function(start, name, keyword_form=True)
|
|
633
|
+
|
|
634
|
+
def finish_function(self, start, name, keyword_form=False):
|
|
635
|
+
self.skip_ws_and_newlines()
|
|
636
|
+
body = self.read_command()
|
|
637
|
+
if body is None:
|
|
638
|
+
self.error("expected a function body")
|
|
639
|
+
return Node("T_Function", start, self.i, name=name, body=body,
|
|
640
|
+
keyword_form=keyword_form)
|
|
641
|
+
|
|
642
|
+
COMPOUND_STARTERS = {"{", "if", "while", "until", "for", "case",
|
|
643
|
+
"select", "[["}
|
|
644
|
+
|
|
645
|
+
def read_bats_test(self):
|
|
646
|
+
"""Parse a bats `@test 'description' { ... }` block."""
|
|
647
|
+
start = self.i
|
|
648
|
+
self.i += 5 # @test
|
|
649
|
+
self.skip_inline_ws()
|
|
650
|
+
desc = self.read_word()
|
|
651
|
+
self.skip_inline_ws()
|
|
652
|
+
if self.peek() != "{":
|
|
653
|
+
self.error("expected { after @test description")
|
|
654
|
+
body = self.read_brace_group()
|
|
655
|
+
return Node("T_BatsTest", start, self.i, description=desc, body=body)
|
|
656
|
+
|
|
657
|
+
def read_coproc(self):
|
|
658
|
+
start = self.i
|
|
659
|
+
self.i += 6
|
|
660
|
+
self.skip_inline_ws()
|
|
661
|
+
name = None
|
|
662
|
+
word = self.peek_literal_word()
|
|
663
|
+
if (word and NAME_RE.fullmatch(word)
|
|
664
|
+
and word not in KEYWORDS):
|
|
665
|
+
# could be NAME compound or the command itself; look ahead
|
|
666
|
+
save = self.i
|
|
667
|
+
self.i += len(word)
|
|
668
|
+
self.skip_inline_ws()
|
|
669
|
+
if (self.src[self.i:self.i + 2] == "(("
|
|
670
|
+
or self.peek() == "("
|
|
671
|
+
or self.peek_literal_word() in self.COMPOUND_STARTERS):
|
|
672
|
+
name = word
|
|
673
|
+
else:
|
|
674
|
+
self.i = save
|
|
675
|
+
cmd = self.read_command()
|
|
676
|
+
if cmd is None:
|
|
677
|
+
self.error("expected a command after coproc")
|
|
678
|
+
return Node("T_CoProc", start, self.i, name=name, command=cmd)
|
|
679
|
+
|
|
680
|
+
def expect_char(self, c):
|
|
681
|
+
self.skip_inline_ws()
|
|
682
|
+
if self.peek() != c:
|
|
683
|
+
self.error("expected %r" % c)
|
|
684
|
+
self.i += 1
|
|
685
|
+
|
|
686
|
+
# ------------------------------------------------------------------
|
|
687
|
+
# Simple commands
|
|
688
|
+
|
|
689
|
+
DECLARE_COMMANDS = {"declare", "typeset", "local", "export", "readonly"}
|
|
690
|
+
|
|
691
|
+
def read_simple_command(self):
|
|
692
|
+
start = self.i
|
|
693
|
+
assigns = []
|
|
694
|
+
words = []
|
|
695
|
+
redirects = []
|
|
696
|
+
is_declare = False
|
|
697
|
+
expecting_cmd = True
|
|
698
|
+
while True:
|
|
699
|
+
self.skip_inline_ws()
|
|
700
|
+
if self.at_end():
|
|
701
|
+
break
|
|
702
|
+
c = self.peek()
|
|
703
|
+
if c in ";&|\n)#":
|
|
704
|
+
if c == "&" and self.peek(1) in (">",):
|
|
705
|
+
pass # &> redirect
|
|
706
|
+
else:
|
|
707
|
+
break
|
|
708
|
+
r = self.maybe_read_redirect()
|
|
709
|
+
if r is not None:
|
|
710
|
+
redirects.append(r)
|
|
711
|
+
continue
|
|
712
|
+
if not words or is_declare:
|
|
713
|
+
a = self.maybe_read_assignment()
|
|
714
|
+
if a is not None:
|
|
715
|
+
assigns.append(a)
|
|
716
|
+
continue
|
|
717
|
+
word = self.read_word()
|
|
718
|
+
if word is None:
|
|
719
|
+
break
|
|
720
|
+
if not words:
|
|
721
|
+
# function definition: name ()
|
|
722
|
+
lit = literal_text(word)
|
|
723
|
+
self.skip_inline_ws()
|
|
724
|
+
if (lit is not None and self.peek() == "("
|
|
725
|
+
and lit not in KEYWORDS
|
|
726
|
+
and not assigns and not redirects):
|
|
727
|
+
save = self.i
|
|
728
|
+
self.i += 1
|
|
729
|
+
self.skip_inline_ws()
|
|
730
|
+
if self.peek() == ")":
|
|
731
|
+
self.i += 1
|
|
732
|
+
return self.finish_function(start, lit)
|
|
733
|
+
self.i = save
|
|
734
|
+
if expecting_cmd:
|
|
735
|
+
lit = literal_text(word)
|
|
736
|
+
if lit in self.DECLARE_COMMANDS:
|
|
737
|
+
is_declare = True
|
|
738
|
+
expecting_cmd = False
|
|
739
|
+
elif lit not in ("builtin", "command"):
|
|
740
|
+
expecting_cmd = False
|
|
741
|
+
words.append(word)
|
|
742
|
+
if not (assigns or words or redirects):
|
|
743
|
+
return None
|
|
744
|
+
return Node("T_SimpleCommand", start, self.i,
|
|
745
|
+
assigns=assigns, words=words, redirects=redirects)
|
|
746
|
+
|
|
747
|
+
def maybe_read_assignment(self):
|
|
748
|
+
"""Parse name=value / name[idx]+=value / name=(...) if present."""
|
|
749
|
+
m = ASSIGN_RE.match(self.src, self.i)
|
|
750
|
+
if not m:
|
|
751
|
+
return None
|
|
752
|
+
start = self.i
|
|
753
|
+
name = m.group(1)
|
|
754
|
+
indices = []
|
|
755
|
+
j = m.end(1)
|
|
756
|
+
# array indices: name[expr]... (only one in bash, but be lenient)
|
|
757
|
+
while j < self.n and self.src[j] == "[":
|
|
758
|
+
depth = 0
|
|
759
|
+
k = j
|
|
760
|
+
while k < self.n:
|
|
761
|
+
ch = self.src[k]
|
|
762
|
+
if ch == "[":
|
|
763
|
+
depth += 1
|
|
764
|
+
elif ch == "]":
|
|
765
|
+
depth -= 1
|
|
766
|
+
if depth == 0:
|
|
767
|
+
break
|
|
768
|
+
elif ch == "\n":
|
|
769
|
+
return None
|
|
770
|
+
k += 1
|
|
771
|
+
else:
|
|
772
|
+
return None
|
|
773
|
+
indices.append(self.src[j + 1:k])
|
|
774
|
+
j = k + 1
|
|
775
|
+
append = False
|
|
776
|
+
if self.src[j:j + 2] == "+=":
|
|
777
|
+
append = True
|
|
778
|
+
j += 2
|
|
779
|
+
elif self.src[j:j + 1] == "=":
|
|
780
|
+
j += 1
|
|
781
|
+
else:
|
|
782
|
+
return None
|
|
783
|
+
self.i = j
|
|
784
|
+
c = self.peek()
|
|
785
|
+
if c == "(":
|
|
786
|
+
value = self.read_array_literal()
|
|
787
|
+
elif c in METACHARS or c == "":
|
|
788
|
+
value = Node("T_NormalWord", self.i, self.i, parts=[])
|
|
789
|
+
else:
|
|
790
|
+
value = self.read_word()
|
|
791
|
+
if value is None:
|
|
792
|
+
value = Node("T_NormalWord", self.i, self.i, parts=[])
|
|
793
|
+
return Node("T_Assignment", start, self.i, name=name,
|
|
794
|
+
indices=indices, append=append, value=value)
|
|
795
|
+
|
|
796
|
+
def read_array_literal(self):
|
|
797
|
+
start = self.i
|
|
798
|
+
self.i += 1 # (
|
|
799
|
+
elements = []
|
|
800
|
+
while True:
|
|
801
|
+
self.skip_ws_and_newlines()
|
|
802
|
+
if self.peek() == ")":
|
|
803
|
+
self.i += 1
|
|
804
|
+
break
|
|
805
|
+
if self.at_end():
|
|
806
|
+
self.error("expected ) to close array literal")
|
|
807
|
+
if self.peek() == "[":
|
|
808
|
+
# possible [idx]=value element
|
|
809
|
+
elem = self.maybe_read_indexed_element()
|
|
810
|
+
if elem is not None:
|
|
811
|
+
elements.append(elem)
|
|
812
|
+
continue
|
|
813
|
+
word = self.read_word()
|
|
814
|
+
if word is None:
|
|
815
|
+
self.error("unexpected token in array literal: %r"
|
|
816
|
+
% self.peek())
|
|
817
|
+
elements.append(word)
|
|
818
|
+
return Node("T_Array", start, self.i, elements=elements)
|
|
819
|
+
|
|
820
|
+
def maybe_read_indexed_element(self):
|
|
821
|
+
start = self.i
|
|
822
|
+
depth = 0
|
|
823
|
+
k = self.i
|
|
824
|
+
while k < self.n:
|
|
825
|
+
ch = self.src[k]
|
|
826
|
+
if ch == "[":
|
|
827
|
+
depth += 1
|
|
828
|
+
elif ch == "]":
|
|
829
|
+
depth -= 1
|
|
830
|
+
if depth == 0:
|
|
831
|
+
break
|
|
832
|
+
elif ch == "\n":
|
|
833
|
+
return None
|
|
834
|
+
k += 1
|
|
835
|
+
else:
|
|
836
|
+
return None
|
|
837
|
+
if self.src[k + 1:k + 2] != "=" and self.src[k + 1:k + 3] != "+=":
|
|
838
|
+
return None
|
|
839
|
+
index = self.src[self.i + 1:k]
|
|
840
|
+
self.i = k + 1
|
|
841
|
+
append = False
|
|
842
|
+
if self.src[self.i:self.i + 2] == "+=":
|
|
843
|
+
append = True
|
|
844
|
+
self.i += 2
|
|
845
|
+
else:
|
|
846
|
+
self.i += 1
|
|
847
|
+
if self.peek() in METACHARS or self.at_end():
|
|
848
|
+
value = Node("T_NormalWord", self.i, self.i, parts=[])
|
|
849
|
+
else:
|
|
850
|
+
value = self.read_word()
|
|
851
|
+
return Node("T_IndexedElement", start, self.i, index=index,
|
|
852
|
+
append=append, value=value)
|
|
853
|
+
|
|
854
|
+
# ------------------------------------------------------------------
|
|
855
|
+
# Redirections
|
|
856
|
+
|
|
857
|
+
def maybe_read_redirect(self):
|
|
858
|
+
src = self.src
|
|
859
|
+
start = self.i
|
|
860
|
+
i = self.i
|
|
861
|
+
fd = None
|
|
862
|
+
m = DIGITS_RE.match(src, i)
|
|
863
|
+
if m and m.end() < self.n and src[m.end()] in "<>":
|
|
864
|
+
fd = m.group(0)
|
|
865
|
+
i = m.end()
|
|
866
|
+
elif src[i:i + 1] == "{":
|
|
867
|
+
m2 = re.match(r"\{([A-Za-z_][A-Za-z0-9_]*)\}(?=[<>])", src[i:])
|
|
868
|
+
if m2:
|
|
869
|
+
fd = "{" + m2.group(1) + "}"
|
|
870
|
+
i += m2.end()
|
|
871
|
+
c = src[i:i + 1]
|
|
872
|
+
if c == "&" and src[i + 1:i + 2] == ">":
|
|
873
|
+
if src[i + 2:i + 3] == ">":
|
|
874
|
+
op, i = "&>>", i + 3
|
|
875
|
+
else:
|
|
876
|
+
op, i = "&>", i + 2
|
|
877
|
+
self.i = i
|
|
878
|
+
self.skip_inline_ws()
|
|
879
|
+
target = self.read_word()
|
|
880
|
+
if target is None:
|
|
881
|
+
self.error("expected a filename after %s" % op)
|
|
882
|
+
return Node("T_FdRedirect", start, self.i, fd=fd,
|
|
883
|
+
op=Node("T_IoFile", start, self.i, op=op,
|
|
884
|
+
file=target))
|
|
885
|
+
if not c or c not in "<>":
|
|
886
|
+
return None
|
|
887
|
+
if c == "<" and src[i + 1:i + 2] == "(" and fd is None:
|
|
888
|
+
return None # process substitution, not a redirect
|
|
889
|
+
if c == ">" and src[i + 1:i + 2] == "(" and fd is None:
|
|
890
|
+
return None
|
|
891
|
+
if c == "<":
|
|
892
|
+
if src[i + 1:i + 2] == "<":
|
|
893
|
+
if src[i + 2:i + 3] == "<":
|
|
894
|
+
self.i = i + 3
|
|
895
|
+
self.skip_inline_ws()
|
|
896
|
+
word = self.read_word()
|
|
897
|
+
if word is None:
|
|
898
|
+
self.error("expected a word after <<<")
|
|
899
|
+
return Node("T_FdRedirect", start, self.i, fd=fd,
|
|
900
|
+
op=Node("T_HereString", start, self.i,
|
|
901
|
+
word=word))
|
|
902
|
+
dashed = src[i + 2:i + 3] == "-"
|
|
903
|
+
self.i = i + 3 if dashed else i + 2
|
|
904
|
+
return self.read_heredoc_marker(start, fd, dashed)
|
|
905
|
+
if src[i + 1:i + 2] == "&":
|
|
906
|
+
self.i = i + 2
|
|
907
|
+
return self.read_dup(start, fd, "<&")
|
|
908
|
+
if src[i + 1:i + 2] == ">":
|
|
909
|
+
op, self.i = "<>", i + 2
|
|
910
|
+
else:
|
|
911
|
+
op, self.i = "<", i + 1
|
|
912
|
+
else:
|
|
913
|
+
if src[i + 1:i + 2] == ">":
|
|
914
|
+
op, self.i = ">>", i + 2
|
|
915
|
+
elif src[i + 1:i + 2] == "&":
|
|
916
|
+
self.i = i + 2
|
|
917
|
+
return self.read_dup(start, fd, ">&")
|
|
918
|
+
elif src[i + 1:i + 2] == "|":
|
|
919
|
+
op, self.i = ">|", i + 2
|
|
920
|
+
else:
|
|
921
|
+
op, self.i = ">", i + 1
|
|
922
|
+
self.skip_inline_ws()
|
|
923
|
+
target = self.read_word()
|
|
924
|
+
if target is None:
|
|
925
|
+
self.error("expected a filename after %s" % op)
|
|
926
|
+
return Node("T_FdRedirect", start, self.i, fd=fd,
|
|
927
|
+
op=Node("T_IoFile", start, self.i, op=op, file=target))
|
|
928
|
+
|
|
929
|
+
def read_dup(self, start, fd, op):
|
|
930
|
+
self.skip_inline_ws()
|
|
931
|
+
m = re.match(r"[0-9]+-?|-", self.src[self.i:])
|
|
932
|
+
if m:
|
|
933
|
+
target = m.group(0)
|
|
934
|
+
self.i += m.end()
|
|
935
|
+
return Node("T_FdRedirect", start, self.i, fd=fd,
|
|
936
|
+
op=Node("T_IoDuplicate", start, self.i, op=op,
|
|
937
|
+
target=target))
|
|
938
|
+
word = self.read_word()
|
|
939
|
+
if word is None:
|
|
940
|
+
self.error("expected a target after %s" % op)
|
|
941
|
+
return Node("T_FdRedirect", start, self.i, fd=fd,
|
|
942
|
+
op=Node("T_IoFile", start, self.i, op=op, file=word))
|
|
943
|
+
|
|
944
|
+
def read_heredoc_marker(self, start, fd, dashed):
|
|
945
|
+
self.skip_inline_ws()
|
|
946
|
+
delim_word = self.read_word()
|
|
947
|
+
if delim_word is None:
|
|
948
|
+
self.error("expected a here document delimiter")
|
|
949
|
+
delimiter, quoted = heredoc_delimiter(delim_word)
|
|
950
|
+
here = Node("T_HereDoc", start, self.i, dashed=dashed,
|
|
951
|
+
quoted=quoted, delimiter=delimiter, parts=[])
|
|
952
|
+
self.pending_heredocs.append(here)
|
|
953
|
+
return Node("T_FdRedirect", start, self.i, fd=fd, op=here)
|
|
954
|
+
|
|
955
|
+
def read_heredoc_bodies(self):
|
|
956
|
+
pending, self.pending_heredocs = self.pending_heredocs, []
|
|
957
|
+
for here in pending:
|
|
958
|
+
lines = []
|
|
959
|
+
content_start = self.i
|
|
960
|
+
while True:
|
|
961
|
+
if self.i >= self.n:
|
|
962
|
+
break
|
|
963
|
+
line_end = self.src.find("\n", self.i)
|
|
964
|
+
if line_end == -1:
|
|
965
|
+
line_end = self.n
|
|
966
|
+
line = self.src[self.i:line_end]
|
|
967
|
+
stripped = line.lstrip("\t") if here.dashed else line
|
|
968
|
+
if stripped == here.delimiter:
|
|
969
|
+
self.i = min(line_end + 1, self.n)
|
|
970
|
+
break
|
|
971
|
+
lines.append(line)
|
|
972
|
+
self.i = min(line_end + 1, self.n)
|
|
973
|
+
content = "\n".join(lines)
|
|
974
|
+
if lines:
|
|
975
|
+
content += "\n"
|
|
976
|
+
if here.quoted:
|
|
977
|
+
parts = [Node("T_Literal", content_start,
|
|
978
|
+
content_start + len(content), text=content)]
|
|
979
|
+
else:
|
|
980
|
+
sub = Parser(content)
|
|
981
|
+
sub.read_dquote_like_parts(content_start)
|
|
982
|
+
parts = sub.heredoc_parts
|
|
983
|
+
here.fields["parts"] = parts
|
|
984
|
+
here.end = self.i
|
|
985
|
+
|
|
986
|
+
def read_dquote_like_parts(self, base):
|
|
987
|
+
"""Parse heredoc content: literal text plus $/`` expansions."""
|
|
988
|
+
parts = []
|
|
989
|
+
src, n = self.src, self.n
|
|
990
|
+
while self.i < n:
|
|
991
|
+
c = src[self.i]
|
|
992
|
+
if c == "$":
|
|
993
|
+
part = self.read_dollar(in_dquote=True)
|
|
994
|
+
if part is not None:
|
|
995
|
+
parts.append(part)
|
|
996
|
+
continue
|
|
997
|
+
parts.append(Node("T_Literal", self.i, self.i + 1, text="$"))
|
|
998
|
+
self.i += 1
|
|
999
|
+
elif c == "`":
|
|
1000
|
+
parts.append(self.read_backticked())
|
|
1001
|
+
elif c == "\\" and self.i + 1 < n and src[self.i + 1] in "$`\\":
|
|
1002
|
+
parts.append(Node("T_Literal", self.i, self.i + 2,
|
|
1003
|
+
text=src[self.i + 1]))
|
|
1004
|
+
self.i += 2
|
|
1005
|
+
else:
|
|
1006
|
+
j = self.i
|
|
1007
|
+
while j < n and src[j] not in "$`\\":
|
|
1008
|
+
j += 1
|
|
1009
|
+
if j == self.i:
|
|
1010
|
+
j += 1
|
|
1011
|
+
parts.append(Node("T_Literal", self.i, j,
|
|
1012
|
+
text=src[self.i:j]))
|
|
1013
|
+
self.i = j
|
|
1014
|
+
for p in parts:
|
|
1015
|
+
shift_node(p, base)
|
|
1016
|
+
self.heredoc_parts = parts
|
|
1017
|
+
|
|
1018
|
+
# ------------------------------------------------------------------
|
|
1019
|
+
# Words
|
|
1020
|
+
|
|
1021
|
+
def read_word(self, stop_chars=""):
|
|
1022
|
+
"""Read a word at the current position; None if no word starts here."""
|
|
1023
|
+
start = self.i
|
|
1024
|
+
parts = []
|
|
1025
|
+
src, n = self.src, self.n
|
|
1026
|
+
while self.i < n:
|
|
1027
|
+
c = src[self.i]
|
|
1028
|
+
if c in METACHARS or c in stop_chars:
|
|
1029
|
+
if c in "<>" and src[self.i + 1:self.i + 2] == "(" \
|
|
1030
|
+
and c not in stop_chars:
|
|
1031
|
+
parts.append(self.read_procsub())
|
|
1032
|
+
continue
|
|
1033
|
+
break
|
|
1034
|
+
if c == "\\":
|
|
1035
|
+
nxt = src[self.i + 1:self.i + 2]
|
|
1036
|
+
if nxt == "\n":
|
|
1037
|
+
self.i += 2
|
|
1038
|
+
continue
|
|
1039
|
+
if nxt == "":
|
|
1040
|
+
parts.append(Node("T_Literal", self.i, self.i + 1,
|
|
1041
|
+
text="\\"))
|
|
1042
|
+
self.i += 1
|
|
1043
|
+
else:
|
|
1044
|
+
parts.append(Node("T_Literal", self.i, self.i + 2,
|
|
1045
|
+
text=nxt, escaped=True))
|
|
1046
|
+
self.i += 2
|
|
1047
|
+
elif c == "'":
|
|
1048
|
+
parts.append(self.read_single_quoted())
|
|
1049
|
+
elif c == '"':
|
|
1050
|
+
parts.append(self.read_double_quoted())
|
|
1051
|
+
elif c == "$":
|
|
1052
|
+
part = self.read_dollar(in_dquote=False)
|
|
1053
|
+
if part is None:
|
|
1054
|
+
parts.append(Node("T_Literal", self.i, self.i + 1,
|
|
1055
|
+
text="$"))
|
|
1056
|
+
self.i += 1
|
|
1057
|
+
else:
|
|
1058
|
+
parts.append(part)
|
|
1059
|
+
elif c == "`":
|
|
1060
|
+
parts.append(self.read_backticked())
|
|
1061
|
+
elif c == "*" or c == "?":
|
|
1062
|
+
if src[self.i + 1:self.i + 2] == "(":
|
|
1063
|
+
parts.append(self.read_extglob())
|
|
1064
|
+
else:
|
|
1065
|
+
parts.append(Node("T_Glob", self.i, self.i + 1, text=c))
|
|
1066
|
+
self.i += 1
|
|
1067
|
+
elif c in "@+!" and src[self.i + 1:self.i + 2] == "(":
|
|
1068
|
+
parts.append(self.read_extglob())
|
|
1069
|
+
elif c == "[":
|
|
1070
|
+
part = self.maybe_read_glob_class()
|
|
1071
|
+
parts.append(part)
|
|
1072
|
+
elif c == "{":
|
|
1073
|
+
part = self.maybe_read_brace_expansion()
|
|
1074
|
+
parts.append(part)
|
|
1075
|
+
elif c == "}" or c == "]" or c == "!" or c == "#" or c == "~" \
|
|
1076
|
+
or c == "@" or c == "+":
|
|
1077
|
+
parts.append(Node("T_Literal", self.i, self.i + 1, text=c))
|
|
1078
|
+
self.i += 1
|
|
1079
|
+
else:
|
|
1080
|
+
m = UNQUOTED_RUN.match(src, self.i)
|
|
1081
|
+
if m:
|
|
1082
|
+
parts.append(Node("T_Literal", self.i, m.end(),
|
|
1083
|
+
text=m.group(0)))
|
|
1084
|
+
self.i = m.end()
|
|
1085
|
+
else:
|
|
1086
|
+
parts.append(Node("T_Literal", self.i, self.i + 1,
|
|
1087
|
+
text=c))
|
|
1088
|
+
self.i += 1
|
|
1089
|
+
if not parts:
|
|
1090
|
+
return None
|
|
1091
|
+
return Node("T_NormalWord", start, self.i, parts=parts)
|
|
1092
|
+
|
|
1093
|
+
def read_single_quoted(self):
|
|
1094
|
+
start = self.i
|
|
1095
|
+
end = self.src.find("'", self.i + 1)
|
|
1096
|
+
if end == -1:
|
|
1097
|
+
self.error("unterminated single-quoted string", start)
|
|
1098
|
+
self.i = end + 1
|
|
1099
|
+
return Node("T_SingleQuoted", start, self.i,
|
|
1100
|
+
text=self.src[start + 1:end])
|
|
1101
|
+
|
|
1102
|
+
def read_double_quoted(self, dollar=False):
|
|
1103
|
+
start = self.i
|
|
1104
|
+
self.i += 1
|
|
1105
|
+
parts = []
|
|
1106
|
+
src, n = self.src, self.n
|
|
1107
|
+
while True:
|
|
1108
|
+
if self.i >= n:
|
|
1109
|
+
self.error("unterminated double-quoted string", start)
|
|
1110
|
+
c = src[self.i]
|
|
1111
|
+
if c == '"':
|
|
1112
|
+
self.i += 1
|
|
1113
|
+
break
|
|
1114
|
+
if c == "$":
|
|
1115
|
+
part = self.read_dollar(in_dquote=True)
|
|
1116
|
+
if part is None:
|
|
1117
|
+
parts.append(Node("T_Literal", self.i, self.i + 1,
|
|
1118
|
+
text="$"))
|
|
1119
|
+
self.i += 1
|
|
1120
|
+
else:
|
|
1121
|
+
parts.append(part)
|
|
1122
|
+
elif c == "`":
|
|
1123
|
+
parts.append(self.read_backticked(in_dquote=True))
|
|
1124
|
+
elif c == "\\":
|
|
1125
|
+
nxt = src[self.i + 1:self.i + 2]
|
|
1126
|
+
if nxt in '"$`\\':
|
|
1127
|
+
parts.append(Node("T_Literal", self.i, self.i + 2,
|
|
1128
|
+
text=nxt, escaped=True))
|
|
1129
|
+
self.i += 2
|
|
1130
|
+
elif nxt == "\n":
|
|
1131
|
+
self.i += 2
|
|
1132
|
+
else:
|
|
1133
|
+
parts.append(Node("T_Literal", self.i, self.i + 1,
|
|
1134
|
+
text="\\"))
|
|
1135
|
+
self.i += 1
|
|
1136
|
+
else:
|
|
1137
|
+
m = DQUOTE_RUN.match(src, self.i)
|
|
1138
|
+
parts.append(Node("T_Literal", self.i, m.end(),
|
|
1139
|
+
text=m.group(0)))
|
|
1140
|
+
self.i = m.end()
|
|
1141
|
+
kind = "T_DollarDoubleQuoted" if dollar else "T_DoubleQuoted"
|
|
1142
|
+
return Node(kind, start, self.i, parts=parts)
|
|
1143
|
+
|
|
1144
|
+
def read_dollar(self, in_dquote):
|
|
1145
|
+
"""Parse a $-construct at i ('$' itself); None if it's a literal $."""
|
|
1146
|
+
src, n = self.src, self.n
|
|
1147
|
+
start = self.i
|
|
1148
|
+
nxt = src[self.i + 1:self.i + 2]
|
|
1149
|
+
if nxt == "(":
|
|
1150
|
+
if src[self.i + 2:self.i + 3] == "(":
|
|
1151
|
+
# try arithmetic, fall back to command substitution
|
|
1152
|
+
save = self.i
|
|
1153
|
+
heredocs = list(self.pending_heredocs)
|
|
1154
|
+
try:
|
|
1155
|
+
self.i += 3
|
|
1156
|
+
expr = self.read_arith_seq()
|
|
1157
|
+
self.skip_inline_ws()
|
|
1158
|
+
if src[self.i:self.i + 2] != "))":
|
|
1159
|
+
self.error("expected ))")
|
|
1160
|
+
self.i += 2
|
|
1161
|
+
return Node("T_DollarArithmetic", start, self.i,
|
|
1162
|
+
expr=expr)
|
|
1163
|
+
except ParseError:
|
|
1164
|
+
self.i = save
|
|
1165
|
+
self.pending_heredocs = heredocs
|
|
1166
|
+
self.i += 2
|
|
1167
|
+
cmds = self.read_list(set())
|
|
1168
|
+
self.skip_ws_and_newlines()
|
|
1169
|
+
if self.peek() != ")":
|
|
1170
|
+
self.error("expected ) to close $( command substitution")
|
|
1171
|
+
self.i += 1
|
|
1172
|
+
return Node("T_DollarExpansion", start, self.i, commands=cmds)
|
|
1173
|
+
if nxt == "{":
|
|
1174
|
+
after = src[self.i + 2:self.i + 3]
|
|
1175
|
+
if after in (" ", "\t", "\n", "|"):
|
|
1176
|
+
# bash 5.3 / mksh "funsub": ${ commands; } and ${| cmd; }
|
|
1177
|
+
self.i += 2
|
|
1178
|
+
if self.peek() == "|":
|
|
1179
|
+
self.i += 1
|
|
1180
|
+
cmds = self.read_list({"}"})
|
|
1181
|
+
self.skip_ws_and_newlines()
|
|
1182
|
+
if self.peek_literal_word() != "}":
|
|
1183
|
+
self.error("expected } to close ${ command; }")
|
|
1184
|
+
self.i += 1
|
|
1185
|
+
return Node("T_DollarBraceCommandExpansion", start, self.i,
|
|
1186
|
+
commands=cmds)
|
|
1187
|
+
return self.read_dollar_braced()
|
|
1188
|
+
if nxt == "'":
|
|
1189
|
+
self.i += 1
|
|
1190
|
+
inner = self.read_ansi_quoted()
|
|
1191
|
+
inner.pos = start
|
|
1192
|
+
return inner
|
|
1193
|
+
if nxt == '"':
|
|
1194
|
+
self.i += 1
|
|
1195
|
+
return self.read_double_quoted(dollar=True)
|
|
1196
|
+
if nxt == "[":
|
|
1197
|
+
# deprecated $[expr]
|
|
1198
|
+
self.i += 2
|
|
1199
|
+
expr = self.read_arith_seq()
|
|
1200
|
+
self.skip_inline_ws()
|
|
1201
|
+
if self.peek() != "]":
|
|
1202
|
+
self.error("expected ] to close $[ ]")
|
|
1203
|
+
self.i += 1
|
|
1204
|
+
return Node("T_DollarArithmetic", start, self.i, expr=expr,
|
|
1205
|
+
deprecated=True)
|
|
1206
|
+
m = NAME_RE.match(src, self.i + 1)
|
|
1207
|
+
if m:
|
|
1208
|
+
self.i = m.end()
|
|
1209
|
+
return Node("T_DollarBraced", start, self.i, braced=False,
|
|
1210
|
+
content=m.group(0))
|
|
1211
|
+
if nxt and nxt in SPECIAL_PARAMS:
|
|
1212
|
+
self.i += 2
|
|
1213
|
+
return Node("T_DollarBraced", start, self.i, braced=False,
|
|
1214
|
+
content=nxt)
|
|
1215
|
+
return None
|
|
1216
|
+
|
|
1217
|
+
def read_dollar_braced(self):
|
|
1218
|
+
start = self.i
|
|
1219
|
+
self.i += 2 # ${
|
|
1220
|
+
src, n = self.src, self.n
|
|
1221
|
+
depth = 1
|
|
1222
|
+
j = self.i
|
|
1223
|
+
while j < n:
|
|
1224
|
+
c = src[j]
|
|
1225
|
+
if c == "\\":
|
|
1226
|
+
j += 2
|
|
1227
|
+
continue
|
|
1228
|
+
if c == "'":
|
|
1229
|
+
end = src.find("'", j + 1)
|
|
1230
|
+
if end == -1:
|
|
1231
|
+
self.error("unterminated string inside ${}", j)
|
|
1232
|
+
j = end + 1
|
|
1233
|
+
continue
|
|
1234
|
+
if c == '"':
|
|
1235
|
+
j += 1
|
|
1236
|
+
while j < n and src[j] != '"':
|
|
1237
|
+
if src[j] == "\\":
|
|
1238
|
+
j += 1
|
|
1239
|
+
j += 1
|
|
1240
|
+
j += 1
|
|
1241
|
+
continue
|
|
1242
|
+
if c == "$" and src[j + 1:j + 2] == "{":
|
|
1243
|
+
depth += 1
|
|
1244
|
+
j += 2
|
|
1245
|
+
continue
|
|
1246
|
+
if c == "}":
|
|
1247
|
+
depth -= 1
|
|
1248
|
+
if depth == 0:
|
|
1249
|
+
break
|
|
1250
|
+
j += 1
|
|
1251
|
+
if j >= n:
|
|
1252
|
+
self.error("unterminated ${", start)
|
|
1253
|
+
content = src[self.i:j]
|
|
1254
|
+
node = Node("T_DollarBraced", start, j + 1, braced=True,
|
|
1255
|
+
content=content)
|
|
1256
|
+
parse_braced_content(node, content, self.i)
|
|
1257
|
+
self.i = j + 1
|
|
1258
|
+
return node
|
|
1259
|
+
|
|
1260
|
+
def read_ansi_quoted(self):
|
|
1261
|
+
start = self.i
|
|
1262
|
+
src, n = self.src, self.n
|
|
1263
|
+
j = self.i + 1
|
|
1264
|
+
while j < n:
|
|
1265
|
+
c = src[j]
|
|
1266
|
+
if c == "\\":
|
|
1267
|
+
j += 2
|
|
1268
|
+
elif c == "'":
|
|
1269
|
+
break
|
|
1270
|
+
else:
|
|
1271
|
+
j += 1
|
|
1272
|
+
if j >= n:
|
|
1273
|
+
self.error("unterminated $' string", start)
|
|
1274
|
+
self.i = j + 1
|
|
1275
|
+
return Node("T_DollarSingleQuoted", start, self.i,
|
|
1276
|
+
text=src[start + 1:j])
|
|
1277
|
+
|
|
1278
|
+
def read_backticked(self, in_dquote=False):
|
|
1279
|
+
start = self.i
|
|
1280
|
+
src, n = self.src, self.n
|
|
1281
|
+
j = self.i + 1
|
|
1282
|
+
escapable = '$`\\"' if in_dquote else "$`\\"
|
|
1283
|
+
chars = []
|
|
1284
|
+
while j < n:
|
|
1285
|
+
c = src[j]
|
|
1286
|
+
if c == "\\" and j + 1 < n and src[j + 1] in escapable:
|
|
1287
|
+
chars.append(src[j + 1])
|
|
1288
|
+
j += 2
|
|
1289
|
+
elif c == "`":
|
|
1290
|
+
break
|
|
1291
|
+
else:
|
|
1292
|
+
chars.append(c)
|
|
1293
|
+
j += 1
|
|
1294
|
+
if j >= n:
|
|
1295
|
+
self.error("unterminated backquote", start)
|
|
1296
|
+
inner = "".join(chars)
|
|
1297
|
+
sub = Parser(inner)
|
|
1298
|
+
try:
|
|
1299
|
+
script = sub.parse()
|
|
1300
|
+
cmds = script.commands
|
|
1301
|
+
except ParseError as e:
|
|
1302
|
+
raise ParseError(e.message, start + 1 + e.pos, e.code)
|
|
1303
|
+
for c in cmds:
|
|
1304
|
+
shift_node(c, start + 1)
|
|
1305
|
+
self.i = j + 1
|
|
1306
|
+
return Node("T_Backticked", start, self.i, commands=cmds)
|
|
1307
|
+
|
|
1308
|
+
def read_procsub(self):
|
|
1309
|
+
start = self.i
|
|
1310
|
+
op = self.src[self.i]
|
|
1311
|
+
self.i += 2 # <( or >(
|
|
1312
|
+
cmds = self.read_list(set())
|
|
1313
|
+
self.skip_ws_and_newlines()
|
|
1314
|
+
if self.peek() != ")":
|
|
1315
|
+
self.error("expected ) to close process substitution")
|
|
1316
|
+
self.i += 1
|
|
1317
|
+
return Node("T_ProcSub", start, self.i, op=op, commands=cmds)
|
|
1318
|
+
|
|
1319
|
+
def read_extglob(self):
|
|
1320
|
+
start = self.i
|
|
1321
|
+
op = self.src[self.i]
|
|
1322
|
+
self.i += 2 # X(
|
|
1323
|
+
items = []
|
|
1324
|
+
current_start = self.i
|
|
1325
|
+
depth = 1
|
|
1326
|
+
# Read the raw contents balancing parens, splitting on top-level |
|
|
1327
|
+
src, n = self.src, self.n
|
|
1328
|
+
j = self.i
|
|
1329
|
+
item_starts = [j]
|
|
1330
|
+
while j < n:
|
|
1331
|
+
c = src[j]
|
|
1332
|
+
if c == "\\":
|
|
1333
|
+
j += 2
|
|
1334
|
+
continue
|
|
1335
|
+
if c == "'":
|
|
1336
|
+
end = src.find("'", j + 1)
|
|
1337
|
+
if end == -1:
|
|
1338
|
+
break
|
|
1339
|
+
j = end + 1
|
|
1340
|
+
continue
|
|
1341
|
+
if c == "(":
|
|
1342
|
+
depth += 1
|
|
1343
|
+
elif c == ")":
|
|
1344
|
+
depth -= 1
|
|
1345
|
+
if depth == 0:
|
|
1346
|
+
break
|
|
1347
|
+
elif c == "|" and depth == 1:
|
|
1348
|
+
items.append(src[item_starts[-1]:j])
|
|
1349
|
+
item_starts.append(j + 1)
|
|
1350
|
+
j += 1
|
|
1351
|
+
if j >= n or src[j] != ")":
|
|
1352
|
+
self.error("unterminated extglob", start)
|
|
1353
|
+
items.append(src[item_starts[-1]:j])
|
|
1354
|
+
self.i = j + 1
|
|
1355
|
+
return Node("T_Extglob", start, self.i, op=op, items=items)
|
|
1356
|
+
|
|
1357
|
+
def maybe_read_glob_class(self):
|
|
1358
|
+
"""Parse [...] as a glob character class, else literal '['."""
|
|
1359
|
+
src, n = self.src, self.n
|
|
1360
|
+
start = self.i
|
|
1361
|
+
j = self.i + 1
|
|
1362
|
+
if j < n and src[j] in "!^":
|
|
1363
|
+
j += 1
|
|
1364
|
+
if j < n and src[j] == "]":
|
|
1365
|
+
j += 1
|
|
1366
|
+
while j < n:
|
|
1367
|
+
c = src[j]
|
|
1368
|
+
if c == "]":
|
|
1369
|
+
self.i = j + 1
|
|
1370
|
+
return Node("T_Glob", start, self.i,
|
|
1371
|
+
text=src[start:self.i])
|
|
1372
|
+
if c in "\n'\"`$\\" or c in METACHARS:
|
|
1373
|
+
break
|
|
1374
|
+
if c == "[" and src[j + 1:j + 2] == ":":
|
|
1375
|
+
end = src.find(":]", j + 2)
|
|
1376
|
+
if end == -1:
|
|
1377
|
+
break
|
|
1378
|
+
j = end + 2
|
|
1379
|
+
continue
|
|
1380
|
+
j += 1
|
|
1381
|
+
self.i = start + 1
|
|
1382
|
+
return Node("T_Literal", start, self.i, text="[")
|
|
1383
|
+
|
|
1384
|
+
def maybe_read_brace_expansion(self):
|
|
1385
|
+
"""Parse {a,b} / {1..5} brace expansion, else literal '{'."""
|
|
1386
|
+
src, n = self.src, self.n
|
|
1387
|
+
start = self.i
|
|
1388
|
+
depth = 0
|
|
1389
|
+
j = self.i
|
|
1390
|
+
has_sep = False
|
|
1391
|
+
while j < n:
|
|
1392
|
+
c = src[j]
|
|
1393
|
+
if c == "\\":
|
|
1394
|
+
j += 2
|
|
1395
|
+
continue
|
|
1396
|
+
if c == "'":
|
|
1397
|
+
end = src.find("'", j + 1)
|
|
1398
|
+
if end == -1:
|
|
1399
|
+
break
|
|
1400
|
+
j = end + 1
|
|
1401
|
+
continue
|
|
1402
|
+
if c in " \t\n;&|<>()" or c == '"' or c == "`":
|
|
1403
|
+
break
|
|
1404
|
+
if c == "{":
|
|
1405
|
+
depth += 1
|
|
1406
|
+
elif c == "}":
|
|
1407
|
+
depth -= 1
|
|
1408
|
+
if depth == 0:
|
|
1409
|
+
j += 1
|
|
1410
|
+
break
|
|
1411
|
+
elif depth == 1 and (c == ","
|
|
1412
|
+
or (c == "." and src[j + 1:j + 2] == ".")):
|
|
1413
|
+
has_sep = True
|
|
1414
|
+
elif c == "$" and src[j + 1:j + 2] == "{":
|
|
1415
|
+
# ${...} inside; skip braced expansion
|
|
1416
|
+
k = j + 2
|
|
1417
|
+
d2 = 1
|
|
1418
|
+
while k < n and d2:
|
|
1419
|
+
if src[k] == "{":
|
|
1420
|
+
d2 += 1
|
|
1421
|
+
elif src[k] == "}":
|
|
1422
|
+
d2 -= 1
|
|
1423
|
+
k += 1
|
|
1424
|
+
j = k - 1
|
|
1425
|
+
j += 1
|
|
1426
|
+
if has_sep and depth == 0 and j > self.i and src[j - 1] == "}":
|
|
1427
|
+
text = src[start:j]
|
|
1428
|
+
inner = text[1:-1]
|
|
1429
|
+
sub = Parser(inner)
|
|
1430
|
+
try:
|
|
1431
|
+
word = sub.read_word()
|
|
1432
|
+
parts = word.parts if word is not None and sub.at_end() \
|
|
1433
|
+
else []
|
|
1434
|
+
except ParseError:
|
|
1435
|
+
parts = []
|
|
1436
|
+
for p in parts:
|
|
1437
|
+
shift_node(p, start + 1)
|
|
1438
|
+
self.i = j
|
|
1439
|
+
return Node("T_BraceExpansion", start, j, text=text,
|
|
1440
|
+
parts=parts)
|
|
1441
|
+
self.i = start + 1
|
|
1442
|
+
return Node("T_Literal", start, self.i, text="{")
|
|
1443
|
+
|
|
1444
|
+
# ------------------------------------------------------------------
|
|
1445
|
+
# Conditions ([ ] and [[ ]])
|
|
1446
|
+
|
|
1447
|
+
def read_condition(self, single):
|
|
1448
|
+
start = self.i
|
|
1449
|
+
self.i += 1 if single else 2
|
|
1450
|
+
self.skip_inline_ws()
|
|
1451
|
+
if self.peek() == "\n" and not single:
|
|
1452
|
+
self.skip_ws_and_newlines()
|
|
1453
|
+
close = "]" if single else "]]"
|
|
1454
|
+
if self.cond_at_close(single):
|
|
1455
|
+
self.consume_cond_close(single)
|
|
1456
|
+
return Node("T_Condition", start, self.i, single=single,
|
|
1457
|
+
expr=Node("TC_Empty", start, self.i))
|
|
1458
|
+
expr = self.read_cond_or(single)
|
|
1459
|
+
self.skip_inline_ws()
|
|
1460
|
+
if not single and self.peek() == "\n":
|
|
1461
|
+
self.skip_ws_and_newlines()
|
|
1462
|
+
if not self.cond_at_close(single):
|
|
1463
|
+
self.error("expected %s" % close)
|
|
1464
|
+
self.consume_cond_close(single)
|
|
1465
|
+
return Node("T_Condition", start, self.i, single=single, expr=expr)
|
|
1466
|
+
|
|
1467
|
+
def cond_at_close(self, single):
|
|
1468
|
+
if single:
|
|
1469
|
+
return (self.peek_literal_word() == "]")
|
|
1470
|
+
return self.src[self.i:self.i + 2] == "]]" and \
|
|
1471
|
+
self.src[self.i + 2:self.i + 3] != "]"
|
|
1472
|
+
|
|
1473
|
+
def consume_cond_close(self, single):
|
|
1474
|
+
self.i += 1 if single else 2
|
|
1475
|
+
|
|
1476
|
+
def cond_skip_ws(self, single):
|
|
1477
|
+
while True:
|
|
1478
|
+
self.skip_inline_ws()
|
|
1479
|
+
if not single and self.peek() == "\n":
|
|
1480
|
+
self.consume_newline()
|
|
1481
|
+
else:
|
|
1482
|
+
break
|
|
1483
|
+
|
|
1484
|
+
def read_cond_or(self, single):
|
|
1485
|
+
left = self.read_cond_and(single)
|
|
1486
|
+
while True:
|
|
1487
|
+
self.cond_skip_ws(single)
|
|
1488
|
+
if self.src[self.i:self.i + 2] == "||":
|
|
1489
|
+
op = "||"
|
|
1490
|
+
self.i += 2
|
|
1491
|
+
elif self.peek_literal_word() == "-o":
|
|
1492
|
+
op = "-o"
|
|
1493
|
+
self.i += 2
|
|
1494
|
+
else:
|
|
1495
|
+
return left
|
|
1496
|
+
self.cond_skip_ws(single)
|
|
1497
|
+
right = self.read_cond_and(single)
|
|
1498
|
+
left = Node("TC_Or", left.pos, right.end, op=op,
|
|
1499
|
+
left=left, right=right)
|
|
1500
|
+
|
|
1501
|
+
def read_cond_and(self, single):
|
|
1502
|
+
left = self.read_cond_term(single)
|
|
1503
|
+
while True:
|
|
1504
|
+
self.cond_skip_ws(single)
|
|
1505
|
+
if self.src[self.i:self.i + 2] == "&&":
|
|
1506
|
+
op = "&&"
|
|
1507
|
+
self.i += 2
|
|
1508
|
+
elif self.peek_literal_word() == "-a":
|
|
1509
|
+
op = "-a"
|
|
1510
|
+
self.i += 2
|
|
1511
|
+
else:
|
|
1512
|
+
return left
|
|
1513
|
+
self.cond_skip_ws(single)
|
|
1514
|
+
right = self.read_cond_term(single)
|
|
1515
|
+
left = Node("TC_And", left.pos, right.end, op=op,
|
|
1516
|
+
left=left, right=right)
|
|
1517
|
+
|
|
1518
|
+
def read_cond_term(self, single):
|
|
1519
|
+
self.cond_skip_ws(single)
|
|
1520
|
+
start = self.i
|
|
1521
|
+
word = self.peek_literal_word()
|
|
1522
|
+
if word == "!":
|
|
1523
|
+
self.i += 1
|
|
1524
|
+
operand = self.read_cond_term(single)
|
|
1525
|
+
return Node("TC_Unary", start, operand.end, op="!",
|
|
1526
|
+
operand=operand)
|
|
1527
|
+
if not single and self.peek() == "(":
|
|
1528
|
+
self.i += 1
|
|
1529
|
+
inner = self.read_cond_or(single)
|
|
1530
|
+
self.cond_skip_ws(single)
|
|
1531
|
+
if self.peek() != ")":
|
|
1532
|
+
self.error("expected ) in [[ ]]")
|
|
1533
|
+
self.i += 1
|
|
1534
|
+
return Node("TC_Group", start, self.i, expr=inner)
|
|
1535
|
+
if single and word == "\\(":
|
|
1536
|
+
self.i += 2
|
|
1537
|
+
inner = self.read_cond_or(single)
|
|
1538
|
+
self.cond_skip_ws(single)
|
|
1539
|
+
if self.peek_literal_word() != "\\)":
|
|
1540
|
+
self.error("expected \\) in [ ]")
|
|
1541
|
+
self.i += 2
|
|
1542
|
+
return Node("TC_Group", start, self.i, expr=inner)
|
|
1543
|
+
if word in UNARY_TEST_OPS or (
|
|
1544
|
+
self.COND_OP_WORD.match(word)
|
|
1545
|
+
and word not in ("-a", "-o")
|
|
1546
|
+
and word not in BINARY_TEST_OPS):
|
|
1547
|
+
self.i += len(word)
|
|
1548
|
+
self.cond_skip_ws(single)
|
|
1549
|
+
operand = self.read_cond_word(single)
|
|
1550
|
+
if operand is None:
|
|
1551
|
+
self.error("expected an operand after %s" % word)
|
|
1552
|
+
return Node("TC_Unary", start, operand.end, op=word,
|
|
1553
|
+
operand=operand)
|
|
1554
|
+
lhs = self.read_cond_word(single)
|
|
1555
|
+
if lhs is None:
|
|
1556
|
+
self.error("expected a test operand")
|
|
1557
|
+
self.cond_skip_ws(single)
|
|
1558
|
+
op = self.peek_cond_binary_op(single)
|
|
1559
|
+
if op is None:
|
|
1560
|
+
return Node("TC_Nullary", start, lhs.end, word=lhs)
|
|
1561
|
+
self.i += getattr(op, "width", len(op))
|
|
1562
|
+
self.cond_skip_ws(single)
|
|
1563
|
+
if op == "=~":
|
|
1564
|
+
rhs = self.read_regex_word()
|
|
1565
|
+
else:
|
|
1566
|
+
rhs = self.read_cond_word(single)
|
|
1567
|
+
if rhs is None:
|
|
1568
|
+
self.error("expected an operand after %s" % op)
|
|
1569
|
+
return Node("TC_Binary", start, rhs.end, op=op, lhs=lhs, rhs=rhs)
|
|
1570
|
+
|
|
1571
|
+
COND_OP_WORD = re.compile(r"-[A-Za-z][A-Za-z0-9]*$")
|
|
1572
|
+
|
|
1573
|
+
def peek_cond_binary_op(self, single):
|
|
1574
|
+
src = self.src
|
|
1575
|
+
for op in ("=~", "==", "!=", "<=", ">=", "\\<=", "\\>=",
|
|
1576
|
+
"\\<", "\\>"):
|
|
1577
|
+
if src.startswith(op, self.i):
|
|
1578
|
+
return op
|
|
1579
|
+
c = src[self.i:self.i + 1]
|
|
1580
|
+
if c in "<>=" and src[self.i + 1:self.i + 2] != "(":
|
|
1581
|
+
return c
|
|
1582
|
+
word = self.peek_literal_word()
|
|
1583
|
+
if word in BINARY_TEST_OPS:
|
|
1584
|
+
return word
|
|
1585
|
+
if (self.COND_OP_WORD.match(word)
|
|
1586
|
+
and word not in UNARY_TEST_OPS
|
|
1587
|
+
and not (single and word in ("-a", "-o"))):
|
|
1588
|
+
# unknown -xy flags: parse as binary so checks can flag them
|
|
1589
|
+
return word
|
|
1590
|
+
if single and c in "'\"":
|
|
1591
|
+
# quoted operator, e.g. [ $a '>' $b ]
|
|
1592
|
+
save = self.i
|
|
1593
|
+
try:
|
|
1594
|
+
w = self.read_word()
|
|
1595
|
+
except ParseError:
|
|
1596
|
+
self.i = save
|
|
1597
|
+
return None
|
|
1598
|
+
self.i = save
|
|
1599
|
+
text = quoted_literal_text(w)
|
|
1600
|
+
if text in BINARY_TEST_OPS:
|
|
1601
|
+
return None if text is None else QuotedOp(text, w.end - save)
|
|
1602
|
+
return None
|
|
1603
|
+
|
|
1604
|
+
def read_cond_word(self, single):
|
|
1605
|
+
if self.cond_at_close(single):
|
|
1606
|
+
return None
|
|
1607
|
+
stop = ")" if not single else ""
|
|
1608
|
+
word = self.read_word(stop_chars=stop)
|
|
1609
|
+
return word
|
|
1610
|
+
|
|
1611
|
+
def read_regex_word(self):
|
|
1612
|
+
"""Read the right-hand side of =~, where ( ) and | are literal."""
|
|
1613
|
+
start = self.i
|
|
1614
|
+
parts = []
|
|
1615
|
+
src, n = self.src, self.n
|
|
1616
|
+
depth = 0
|
|
1617
|
+
while self.i < n:
|
|
1618
|
+
c = src[self.i]
|
|
1619
|
+
if c == "[":
|
|
1620
|
+
# bracket expression: ]] inside it is not the closer
|
|
1621
|
+
j = self.i + 1
|
|
1622
|
+
if j < n and src[j] == "^":
|
|
1623
|
+
j += 1
|
|
1624
|
+
if j < n and src[j] == "]":
|
|
1625
|
+
j += 1
|
|
1626
|
+
while j < n and src[j] != "]":
|
|
1627
|
+
if src.startswith("[:", j):
|
|
1628
|
+
k = src.find(":]", j + 2)
|
|
1629
|
+
if k == -1:
|
|
1630
|
+
break
|
|
1631
|
+
j = k + 2
|
|
1632
|
+
else:
|
|
1633
|
+
j += 1
|
|
1634
|
+
if j < n and src[j] == "]":
|
|
1635
|
+
parts.append(Node("T_Literal", self.i, j + 1,
|
|
1636
|
+
text=src[self.i:j + 1]))
|
|
1637
|
+
self.i = j + 1
|
|
1638
|
+
continue
|
|
1639
|
+
if c in " \t\n" and depth == 0:
|
|
1640
|
+
break
|
|
1641
|
+
if src.startswith("]]", self.i) and depth == 0:
|
|
1642
|
+
break
|
|
1643
|
+
if (src.startswith("&&", self.i) or
|
|
1644
|
+
src.startswith("||", self.i)) and depth == 0:
|
|
1645
|
+
break
|
|
1646
|
+
if c == "(":
|
|
1647
|
+
depth += 1
|
|
1648
|
+
parts.append(Node("T_Literal", self.i, self.i + 1, text="("))
|
|
1649
|
+
self.i += 1
|
|
1650
|
+
elif c == ")":
|
|
1651
|
+
if depth == 0:
|
|
1652
|
+
break
|
|
1653
|
+
depth -= 1
|
|
1654
|
+
parts.append(Node("T_Literal", self.i, self.i + 1, text=")"))
|
|
1655
|
+
self.i += 1
|
|
1656
|
+
elif c == "'":
|
|
1657
|
+
parts.append(self.read_single_quoted())
|
|
1658
|
+
elif c == '"':
|
|
1659
|
+
parts.append(self.read_double_quoted())
|
|
1660
|
+
elif c == "$":
|
|
1661
|
+
part = self.read_dollar(in_dquote=False)
|
|
1662
|
+
if part is None:
|
|
1663
|
+
parts.append(Node("T_Literal", self.i, self.i + 1,
|
|
1664
|
+
text="$"))
|
|
1665
|
+
self.i += 1
|
|
1666
|
+
else:
|
|
1667
|
+
parts.append(part)
|
|
1668
|
+
elif c == "\\":
|
|
1669
|
+
nxt = src[self.i + 1:self.i + 2]
|
|
1670
|
+
parts.append(Node("T_Literal", self.i, self.i + 2,
|
|
1671
|
+
text=nxt, escaped=True))
|
|
1672
|
+
self.i += 2
|
|
1673
|
+
else:
|
|
1674
|
+
parts.append(Node("T_Literal", self.i, self.i + 1, text=c))
|
|
1675
|
+
self.i += 1
|
|
1676
|
+
if not parts:
|
|
1677
|
+
return None
|
|
1678
|
+
return Node("T_NormalWord", start, self.i, parts=parts)
|
|
1679
|
+
|
|
1680
|
+
# ------------------------------------------------------------------
|
|
1681
|
+
# Arithmetic expressions
|
|
1682
|
+
|
|
1683
|
+
def read_arith_seq(self, allow_empty=False):
|
|
1684
|
+
start = self.i
|
|
1685
|
+
exprs = [self.read_arith_assignment(allow_empty=allow_empty)]
|
|
1686
|
+
while True:
|
|
1687
|
+
self.skip_arith_ws()
|
|
1688
|
+
if self.peek() == ",":
|
|
1689
|
+
self.i += 1
|
|
1690
|
+
exprs.append(self.read_arith_assignment())
|
|
1691
|
+
else:
|
|
1692
|
+
break
|
|
1693
|
+
if len(exprs) == 1:
|
|
1694
|
+
return exprs[0]
|
|
1695
|
+
return Node("TA_Sequence", start, self.i, exprs=exprs)
|
|
1696
|
+
|
|
1697
|
+
def skip_arith_ws(self):
|
|
1698
|
+
src, n = self.src, self.n
|
|
1699
|
+
i = self.i
|
|
1700
|
+
while i < n:
|
|
1701
|
+
c = src[i]
|
|
1702
|
+
if c in " \t\r\n":
|
|
1703
|
+
i += 1
|
|
1704
|
+
elif c == "\\" and i + 1 < n and src[i + 1] == "\n":
|
|
1705
|
+
i += 2
|
|
1706
|
+
else:
|
|
1707
|
+
break
|
|
1708
|
+
self.i = i
|
|
1709
|
+
|
|
1710
|
+
def read_arith_assignment(self, allow_empty=False):
|
|
1711
|
+
self.skip_arith_ws()
|
|
1712
|
+
start = self.i
|
|
1713
|
+
if allow_empty and (self.peek() in ";)" or
|
|
1714
|
+
self.src[self.i:self.i + 2] == "))"):
|
|
1715
|
+
return Node("TA_Empty", start, start)
|
|
1716
|
+
left = self.read_arith_ternary()
|
|
1717
|
+
self.skip_arith_ws()
|
|
1718
|
+
src = self.src
|
|
1719
|
+
for op in ARITH_ASSIGN_OPS:
|
|
1720
|
+
if src.startswith(op, self.i):
|
|
1721
|
+
if op == "=" and src[self.i + 1:self.i + 2] == "=":
|
|
1722
|
+
break
|
|
1723
|
+
self.i += len(op)
|
|
1724
|
+
right = self.read_arith_assignment()
|
|
1725
|
+
return Node("TA_Assignment", start, right.end, op=op,
|
|
1726
|
+
left=left, right=right)
|
|
1727
|
+
return left
|
|
1728
|
+
|
|
1729
|
+
def read_arith_ternary(self):
|
|
1730
|
+
start = self.i
|
|
1731
|
+
cond = self.read_arith_binary(0)
|
|
1732
|
+
self.skip_arith_ws()
|
|
1733
|
+
if self.peek() == "?":
|
|
1734
|
+
self.i += 1
|
|
1735
|
+
then = self.read_arith_assignment()
|
|
1736
|
+
self.skip_arith_ws()
|
|
1737
|
+
if self.peek() != ":":
|
|
1738
|
+
self.error("expected : in ternary expression")
|
|
1739
|
+
self.i += 1
|
|
1740
|
+
otherwise = self.read_arith_assignment()
|
|
1741
|
+
return Node("TA_Trinary", start, otherwise.end, cond=cond,
|
|
1742
|
+
then=then, otherwise=otherwise)
|
|
1743
|
+
return cond
|
|
1744
|
+
|
|
1745
|
+
ARITH_BINARY_LEVELS = [
|
|
1746
|
+
("||",),
|
|
1747
|
+
("&&",),
|
|
1748
|
+
("|",),
|
|
1749
|
+
("^",),
|
|
1750
|
+
("&",),
|
|
1751
|
+
("==", "!="),
|
|
1752
|
+
("<=", ">=", "<", ">"),
|
|
1753
|
+
("<<", ">>"),
|
|
1754
|
+
("+", "-"),
|
|
1755
|
+
("*", "/", "%"),
|
|
1756
|
+
]
|
|
1757
|
+
|
|
1758
|
+
def read_arith_binary(self, level):
|
|
1759
|
+
if level >= len(self.ARITH_BINARY_LEVELS):
|
|
1760
|
+
return self.read_arith_power()
|
|
1761
|
+
ops = self.ARITH_BINARY_LEVELS[level]
|
|
1762
|
+
start = self.i
|
|
1763
|
+
left = self.read_arith_binary(level + 1)
|
|
1764
|
+
while True:
|
|
1765
|
+
self.skip_arith_ws()
|
|
1766
|
+
src = self.src
|
|
1767
|
+
matched = None
|
|
1768
|
+
for op in ops:
|
|
1769
|
+
if src.startswith(op, self.i):
|
|
1770
|
+
after = src[self.i + len(op):self.i + len(op) + 1]
|
|
1771
|
+
if op in ("<", ">") and after == op:
|
|
1772
|
+
continue
|
|
1773
|
+
if op in ("<", ">") and after == "=":
|
|
1774
|
+
continue
|
|
1775
|
+
if op == "|" and after == "|":
|
|
1776
|
+
continue
|
|
1777
|
+
if op == "&" and after == "&":
|
|
1778
|
+
continue
|
|
1779
|
+
if after == "=" and op in ("+", "-", "*", "/", "%",
|
|
1780
|
+
"&", "|", "^", "<<", ">>"):
|
|
1781
|
+
continue # compound assignment, not binary op
|
|
1782
|
+
matched = op
|
|
1783
|
+
break
|
|
1784
|
+
if matched is None:
|
|
1785
|
+
return left
|
|
1786
|
+
if matched in ("+", "-"):
|
|
1787
|
+
nxt = src[self.i + 1:self.i + 2]
|
|
1788
|
+
if nxt == matched:
|
|
1789
|
+
# could be a++ + b or ++b; if left side just ended with
|
|
1790
|
+
# operand, this is postfix on it
|
|
1791
|
+
pass
|
|
1792
|
+
self.i += len(matched)
|
|
1793
|
+
right = self.read_arith_binary(level + 1)
|
|
1794
|
+
left = Node("TA_Binary", start, right.end, op=matched,
|
|
1795
|
+
left=left, right=right)
|
|
1796
|
+
|
|
1797
|
+
def read_arith_power(self):
|
|
1798
|
+
start = self.i
|
|
1799
|
+
left = self.read_arith_unary()
|
|
1800
|
+
self.skip_arith_ws()
|
|
1801
|
+
if self.src.startswith("**", self.i):
|
|
1802
|
+
self.i += 2
|
|
1803
|
+
right = self.read_arith_power()
|
|
1804
|
+
return Node("TA_Binary", start, right.end, op="**", left=left,
|
|
1805
|
+
right=right)
|
|
1806
|
+
return left
|
|
1807
|
+
|
|
1808
|
+
def read_arith_unary(self):
|
|
1809
|
+
self.skip_arith_ws()
|
|
1810
|
+
start = self.i
|
|
1811
|
+
src = self.src
|
|
1812
|
+
for op in ("++", "--", "!", "~", "+", "-"):
|
|
1813
|
+
if src.startswith(op, self.i):
|
|
1814
|
+
if op in ("+", "-") and src.startswith(op * 2, self.i):
|
|
1815
|
+
continue
|
|
1816
|
+
self.i += len(op)
|
|
1817
|
+
operand = self.read_arith_unary()
|
|
1818
|
+
kind = "TA_Unary"
|
|
1819
|
+
return Node(kind, start, operand.end, op=op, operand=operand)
|
|
1820
|
+
return self.read_arith_postfix()
|
|
1821
|
+
|
|
1822
|
+
def read_arith_postfix(self):
|
|
1823
|
+
start = self.i
|
|
1824
|
+
operand = self.read_arith_primary()
|
|
1825
|
+
while True:
|
|
1826
|
+
self.skip_arith_ws()
|
|
1827
|
+
if self.src.startswith("++", self.i) or \
|
|
1828
|
+
self.src.startswith("--", self.i):
|
|
1829
|
+
op = self.src[self.i:self.i + 2]
|
|
1830
|
+
self.i += 2
|
|
1831
|
+
operand = Node("TA_Unary", start, self.i, op="|" + op,
|
|
1832
|
+
operand=operand, postfix=True)
|
|
1833
|
+
else:
|
|
1834
|
+
return operand
|
|
1835
|
+
|
|
1836
|
+
def read_arith_primary(self):
|
|
1837
|
+
self.skip_arith_ws()
|
|
1838
|
+
start = self.i
|
|
1839
|
+
src, n = self.src, self.n
|
|
1840
|
+
c = self.peek()
|
|
1841
|
+
if c == "(":
|
|
1842
|
+
self.i += 1
|
|
1843
|
+
inner = self.read_arith_seq()
|
|
1844
|
+
self.skip_arith_ws()
|
|
1845
|
+
if self.peek() != ")":
|
|
1846
|
+
self.error("expected ) in arithmetic expression")
|
|
1847
|
+
self.i += 1
|
|
1848
|
+
return Node("TA_Parenthesis", start, self.i, expr=inner)
|
|
1849
|
+
if c == "$":
|
|
1850
|
+
part = self.read_dollar(in_dquote=False)
|
|
1851
|
+
if part is not None:
|
|
1852
|
+
indices = self.maybe_arith_indices()
|
|
1853
|
+
return Node("TA_Expansion", start, self.i, parts=[part],
|
|
1854
|
+
indices=indices)
|
|
1855
|
+
if c == "`":
|
|
1856
|
+
part = self.read_backticked()
|
|
1857
|
+
return Node("TA_Expansion", start, self.i, parts=[part],
|
|
1858
|
+
indices=[])
|
|
1859
|
+
if c == '"' or c == "'":
|
|
1860
|
+
part = (self.read_double_quoted() if c == '"'
|
|
1861
|
+
else self.read_single_quoted())
|
|
1862
|
+
return Node("TA_Expansion", start, self.i, parts=[part],
|
|
1863
|
+
indices=[])
|
|
1864
|
+
m = re.match(r"0[xX][0-9a-fA-F]+|[0-9]+#[0-9a-zA-Z@_]+|[0-9]+",
|
|
1865
|
+
src[self.i:])
|
|
1866
|
+
if m:
|
|
1867
|
+
self.i += m.end()
|
|
1868
|
+
return Node("TA_Literal", start, self.i, value=m.group(0))
|
|
1869
|
+
m = NAME_RE.match(src, self.i)
|
|
1870
|
+
if m:
|
|
1871
|
+
name = m.group(0)
|
|
1872
|
+
self.i = m.end()
|
|
1873
|
+
indices = self.maybe_arith_indices()
|
|
1874
|
+
return Node("TA_Variable", start, self.i, name=name,
|
|
1875
|
+
indices=indices)
|
|
1876
|
+
self.error("unexpected token in arithmetic expression: %r"
|
|
1877
|
+
% (c or "<eof>"))
|
|
1878
|
+
|
|
1879
|
+
def maybe_arith_indices(self):
|
|
1880
|
+
indices = []
|
|
1881
|
+
src, n = self.src, self.n
|
|
1882
|
+
while self.peek() == "[":
|
|
1883
|
+
depth = 0
|
|
1884
|
+
j = self.i
|
|
1885
|
+
while j < n:
|
|
1886
|
+
ch = src[j]
|
|
1887
|
+
if ch == "[":
|
|
1888
|
+
depth += 1
|
|
1889
|
+
elif ch == "]":
|
|
1890
|
+
depth -= 1
|
|
1891
|
+
if depth == 0:
|
|
1892
|
+
break
|
|
1893
|
+
elif ch == "\\":
|
|
1894
|
+
j += 1
|
|
1895
|
+
j += 1
|
|
1896
|
+
if j >= n:
|
|
1897
|
+
self.error("unterminated array index")
|
|
1898
|
+
raw = src[self.i + 1:j]
|
|
1899
|
+
inner_pos = self.i + 1
|
|
1900
|
+
self.i = j + 1
|
|
1901
|
+
indices.append(parse_index(raw, inner_pos))
|
|
1902
|
+
return indices
|
|
1903
|
+
|
|
1904
|
+
|
|
1905
|
+
# ----------------------------------------------------------------------
|
|
1906
|
+
# Helpers
|
|
1907
|
+
|
|
1908
|
+
def shift_node(node, offset):
|
|
1909
|
+
node.pos += offset
|
|
1910
|
+
node.end += offset
|
|
1911
|
+
from .shast import iter_children
|
|
1912
|
+
for c in iter_children(node):
|
|
1913
|
+
shift_node(c, offset)
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
def literal_text(word):
|
|
1917
|
+
"""The literal string of a word made only of literal parts, else None."""
|
|
1918
|
+
if word is None or word.kind != "T_NormalWord":
|
|
1919
|
+
return None
|
|
1920
|
+
out = []
|
|
1921
|
+
for p in word.parts:
|
|
1922
|
+
if p.kind == "T_Literal":
|
|
1923
|
+
out.append(p.text)
|
|
1924
|
+
else:
|
|
1925
|
+
return None
|
|
1926
|
+
return "".join(out)
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
def heredoc_delimiter(word):
|
|
1930
|
+
"""Extract (delimiter_text, was_quoted) from a heredoc delimiter word."""
|
|
1931
|
+
out = []
|
|
1932
|
+
quoted = False
|
|
1933
|
+
for p in word.parts:
|
|
1934
|
+
if p.kind == "T_Literal":
|
|
1935
|
+
out.append(p.text)
|
|
1936
|
+
if p.get("escaped"):
|
|
1937
|
+
quoted = True
|
|
1938
|
+
elif p.kind == "T_SingleQuoted":
|
|
1939
|
+
out.append(p.text)
|
|
1940
|
+
quoted = True
|
|
1941
|
+
elif p.kind in ("T_DoubleQuoted", "T_DollarDoubleQuoted"):
|
|
1942
|
+
quoted = True
|
|
1943
|
+
for q in p.parts:
|
|
1944
|
+
if q.kind == "T_Literal":
|
|
1945
|
+
out.append(q.text)
|
|
1946
|
+
elif p.kind == "T_DollarSingleQuoted":
|
|
1947
|
+
out.append(p.text)
|
|
1948
|
+
quoted = True
|
|
1949
|
+
else:
|
|
1950
|
+
out.append("?")
|
|
1951
|
+
return "".join(out), quoted
|
|
1952
|
+
|
|
1953
|
+
|
|
1954
|
+
BRACED_RE = re.compile(
|
|
1955
|
+
r"^(?P<bang>[!#]?)"
|
|
1956
|
+
r"(?P<name>[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[@*#?$!_-])?"
|
|
1957
|
+
r"(?P<index>\[.*?\])*"
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
BRACED_OPS = ("::", ":-", ":=", ":?", ":+", "##", "%%", "//", "/#", "/%",
|
|
1961
|
+
"^^", ",,", "@", "-", "=", "?", "+", "#", "%", "/", "^", ",",
|
|
1962
|
+
":")
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
def parse_braced_content(node, content, base):
|
|
1966
|
+
"""Decompose ${...} content into name / op / argument word."""
|
|
1967
|
+
f = node.fields
|
|
1968
|
+
f["prefix"] = ""
|
|
1969
|
+
f["op"] = ""
|
|
1970
|
+
f["arg_parts"] = []
|
|
1971
|
+
f["indices"] = []
|
|
1972
|
+
s = content
|
|
1973
|
+
if not s:
|
|
1974
|
+
f["name"] = ""
|
|
1975
|
+
return
|
|
1976
|
+
i = 0
|
|
1977
|
+
if s[0] in "!#" and len(s) > 1 and (s[1].isalnum() or s[1] in "_@*?$!#-"):
|
|
1978
|
+
# ${#var} length, ${!var} indirection -- but ${#} and ${!} are names
|
|
1979
|
+
f["prefix"] = s[0]
|
|
1980
|
+
i = 1
|
|
1981
|
+
m = re.match(r"[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[@*#?$!_-]", s[i:])
|
|
1982
|
+
if not m:
|
|
1983
|
+
f["name"] = ""
|
|
1984
|
+
return
|
|
1985
|
+
f["name"] = m.group(0)
|
|
1986
|
+
i += m.end()
|
|
1987
|
+
# array indices
|
|
1988
|
+
while i < len(s) and s[i] == "[":
|
|
1989
|
+
depth = 0
|
|
1990
|
+
j = i
|
|
1991
|
+
while j < len(s):
|
|
1992
|
+
if s[j] == "[":
|
|
1993
|
+
depth += 1
|
|
1994
|
+
elif s[j] == "]":
|
|
1995
|
+
depth -= 1
|
|
1996
|
+
if depth == 0:
|
|
1997
|
+
break
|
|
1998
|
+
j += 1
|
|
1999
|
+
if j >= len(s):
|
|
2000
|
+
break
|
|
2001
|
+
f["indices"].append(parse_index(s[i + 1:j], base + i + 1))
|
|
2002
|
+
i = j + 1
|
|
2003
|
+
for op in BRACED_OPS:
|
|
2004
|
+
if s.startswith(op, i):
|
|
2005
|
+
f["op"] = op
|
|
2006
|
+
i += len(op)
|
|
2007
|
+
break
|
|
2008
|
+
rest = s[i:]
|
|
2009
|
+
if rest:
|
|
2010
|
+
sub = Parser(rest)
|
|
2011
|
+
try:
|
|
2012
|
+
word = sub.read_word(stop_chars="")
|
|
2013
|
+
except ParseError:
|
|
2014
|
+
word = None
|
|
2015
|
+
if word is not None and sub.at_end():
|
|
2016
|
+
shift_node(word, base + i)
|
|
2017
|
+
f["arg_parts"] = word.parts
|
|
2018
|
+
else:
|
|
2019
|
+
f["arg_parts"] = [Node("T_Literal", base + i,
|
|
2020
|
+
base + i + len(rest), text=rest)]
|
|
2021
|
+
|
|
2022
|
+
|
|
2023
|
+
def parse_index(raw, pos):
|
|
2024
|
+
"""Parse an array index as arithmetic when possible, else literal."""
|
|
2025
|
+
raw_stripped = raw.strip()
|
|
2026
|
+
if raw_stripped in ("@", "*"):
|
|
2027
|
+
return Node("T_Literal", pos, pos + len(raw), text=raw_stripped)
|
|
2028
|
+
sub = Parser(raw)
|
|
2029
|
+
try:
|
|
2030
|
+
expr = sub.read_arith_seq()
|
|
2031
|
+
sub.skip_arith_ws()
|
|
2032
|
+
if sub.at_end():
|
|
2033
|
+
shift_node(expr, pos)
|
|
2034
|
+
return expr
|
|
2035
|
+
except ParseError:
|
|
2036
|
+
pass
|
|
2037
|
+
sub = Parser(raw)
|
|
2038
|
+
try:
|
|
2039
|
+
word = sub.read_word()
|
|
2040
|
+
if word is not None and sub.at_end():
|
|
2041
|
+
shift_node(word, pos)
|
|
2042
|
+
return word
|
|
2043
|
+
except ParseError:
|
|
2044
|
+
pass
|
|
2045
|
+
return Node("T_Literal", pos, pos + len(raw), text=raw)
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
def parse(source):
|
|
2049
|
+
"""Parse a script, returning its T_Script root node."""
|
|
2050
|
+
return Parser(source).parse()
|