pureshellcheck 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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()