@cplieger/deadset-ts 8.0.0-dev.11 → 8.0.0-dev.13
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.
- package/README.md +2 -0
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/dist/src/build-scripts.js +6 -13
- package/dist/src/commands/read.js +156 -0
- package/dist/src/commands/shell.js +75 -0
- package/dist/src/commands/tools.js +460 -0
- package/dist/src/configuration-files.js +11 -25
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/dependencies.js +67 -115
- package/dist/src/load/synthesize.js +12 -5
- package/dist/src/manifest.js +27 -14
- package/dist/src/roots.js +18 -11
- package/dist/src/text/named-files.js +24 -3
- package/dist/src/text/yaml.js +464 -173
- package/dist/src/vendor/unbash/ansi-c.js +132 -0
- package/dist/src/vendor/unbash/arithmetic-parts.js +37 -0
- package/dist/src/vendor/unbash/arithmetic.js +591 -0
- package/dist/src/vendor/unbash/assignment.js +131 -0
- package/dist/src/vendor/unbash/chars.js +48 -0
- package/dist/src/vendor/unbash/command.js +41 -0
- package/dist/src/vendor/unbash/expansion.js +80 -0
- package/dist/src/vendor/unbash/glob.js +268 -0
- package/dist/src/vendor/unbash/heredoc.js +27 -0
- package/dist/src/vendor/unbash/internal-types.js +1 -0
- package/dist/src/vendor/unbash/lexer.js +4274 -0
- package/dist/src/vendor/unbash/limits.js +3 -0
- package/dist/src/vendor/unbash/parser.js +1366 -0
- package/dist/src/vendor/unbash/parts.js +57 -0
- package/dist/src/vendor/unbash/substitution-context.js +8 -0
- package/dist/src/vendor/unbash/traversal.js +212 -0
- package/dist/src/vendor/unbash/types.js +1 -0
- package/dist/src/vendor/unbash/word-value.js +61 -0
- package/dist/src/vendor/unbash/word.js +40 -0
- package/dist/src/workflow-steps.js +111 -133
- package/jsr.json +2 -1
- package/package.json +9 -7
- package/src/build-scripts.ts +6 -13
- package/src/commands/read.ts +230 -0
- package/src/commands/shell.ts +126 -0
- package/src/commands/tools.ts +618 -0
- package/src/configuration-files.ts +10 -26
- package/src/conformance-record.ts +1 -1
- package/src/dependencies.ts +99 -123
- package/src/load/synthesize.ts +18 -5
- package/src/manifest.ts +43 -25
- package/src/roots.ts +20 -13
- package/src/text/named-files.ts +26 -3
- package/src/text/yaml.ts +503 -179
- package/src/vendor/unbash/LICENSE +15 -0
- package/src/vendor/unbash/VENDORED.md +16 -0
- package/src/vendor/unbash/ansi-c.ts +132 -0
- package/src/vendor/unbash/arithmetic-parts.ts +53 -0
- package/src/vendor/unbash/arithmetic.ts +641 -0
- package/src/vendor/unbash/assignment.ts +163 -0
- package/src/vendor/unbash/chars.ts +48 -0
- package/src/vendor/unbash/command.ts +47 -0
- package/src/vendor/unbash/expansion.ts +144 -0
- package/src/vendor/unbash/glob.ts +284 -0
- package/src/vendor/unbash/heredoc.ts +34 -0
- package/src/vendor/unbash/internal-types.ts +94 -0
- package/src/vendor/unbash/lexer.ts +4352 -0
- package/src/vendor/unbash/limits.ts +3 -0
- package/src/vendor/unbash/parser.ts +1612 -0
- package/src/vendor/unbash/parts.ts +96 -0
- package/src/vendor/unbash/substitution-context.ts +20 -0
- package/src/vendor/unbash/traversal.ts +182 -0
- package/src/vendor/unbash/types.ts +660 -0
- package/src/vendor/unbash/word-value.ts +64 -0
- package/src/vendor/unbash/word.ts +50 -0
- package/src/workflow-steps.ts +153 -154
- package/dist/src/text/shell.js +0 -159
- package/src/text/named-files.inputs.json +0 -1522
- package/src/text/shell.ts +0 -161
|
@@ -0,0 +1,4274 @@
|
|
|
1
|
+
import { decodeAnsiCQuoted } from "./ansi-c.js";
|
|
2
|
+
import { ArithmeticExpansionImpl, ProcessSubstitutionImpl, ScriptExpansionImpl, } from "./expansion.js";
|
|
3
|
+
import { MAX_SYNTAX_NESTING } from "./limits.js";
|
|
4
|
+
import { WordImpl } from "./word.js";
|
|
5
|
+
import { createSubstitutionContext } from "./substitution-context.js";
|
|
6
|
+
import { hasGlobCharacters, parseGlobParts } from "./glob.js";
|
|
7
|
+
import { skipLineContinuations, CH_TAB, CH_NL, CH_SPACE, CH_BANG, CH_DQUOTE, CH_HASH, CH_DOLLAR, CH_PERCENT, CH_AMP, CH_SQUOTE, CH_LPAREN, CH_RPAREN, CH_STAR, CH_PLUS, CH_COMMA, CH_DASH, CH_SLASH, CH_0, CH_9, CH_COLON, CH_SEMI, CH_LT, CH_EQ, CH_GT, CH_QUESTION, CH_AT, CH_A, CH_Z, CH_LBRACKET, CH_BACKSLASH, CH_RBRACKET, CH_CARET, CH_UNDERSCORE, CH_BACKTICK, CH_a, CH_z, CH_LBRACE, CH_PIPE, CH_RBRACE, } from "./chars.js";
|
|
8
|
+
export const Token = {
|
|
9
|
+
Word: 0,
|
|
10
|
+
Assignment: 1,
|
|
11
|
+
Semi: 2,
|
|
12
|
+
Newline: 3,
|
|
13
|
+
Pipe: 4,
|
|
14
|
+
And: 5,
|
|
15
|
+
Or: 6,
|
|
16
|
+
Amp: 7,
|
|
17
|
+
LParen: 8,
|
|
18
|
+
RParen: 9,
|
|
19
|
+
LBrace: 10,
|
|
20
|
+
RBrace: 11,
|
|
21
|
+
Bang: 12,
|
|
22
|
+
If: 13,
|
|
23
|
+
Then: 14,
|
|
24
|
+
Else: 15,
|
|
25
|
+
Elif: 16,
|
|
26
|
+
Fi: 17,
|
|
27
|
+
Do: 18,
|
|
28
|
+
Done: 19,
|
|
29
|
+
For: 20,
|
|
30
|
+
While: 21,
|
|
31
|
+
Until: 22,
|
|
32
|
+
In: 23,
|
|
33
|
+
Case: 24,
|
|
34
|
+
Esac: 25,
|
|
35
|
+
Function: 26,
|
|
36
|
+
DoubleSemi: 27,
|
|
37
|
+
SemiAmp: 28,
|
|
38
|
+
DoubleSemiAmp: 29,
|
|
39
|
+
Select: 30,
|
|
40
|
+
DblLBracket: 31,
|
|
41
|
+
DblRBracket: 32,
|
|
42
|
+
EOF: 33,
|
|
43
|
+
ArithCmd: 34,
|
|
44
|
+
Coproc: 35,
|
|
45
|
+
Redirect: 36,
|
|
46
|
+
};
|
|
47
|
+
export class TokenValue {
|
|
48
|
+
token = Token.EOF;
|
|
49
|
+
// Materialized token value, or null for word tokens whose value has not been
|
|
50
|
+
// requested yet (computed from [pos, end) on demand via the owning lexer).
|
|
51
|
+
_value = "";
|
|
52
|
+
_owner;
|
|
53
|
+
pos = 0;
|
|
54
|
+
end = 0;
|
|
55
|
+
fileDescriptor = undefined;
|
|
56
|
+
variableName = undefined;
|
|
57
|
+
descriptorEnd = 0;
|
|
58
|
+
delimiterQuoted = false;
|
|
59
|
+
content = undefined;
|
|
60
|
+
targetPos = 0;
|
|
61
|
+
targetEnd = 0;
|
|
62
|
+
assignmentOperatorPos = -1;
|
|
63
|
+
// True when `value` is exactly the raw source span [pos, end) — lets consumers
|
|
64
|
+
// reuse the string instead of slicing the source again.
|
|
65
|
+
raw = false;
|
|
66
|
+
// True when a word contains no quoting, escaped characters, or expansions.
|
|
67
|
+
// Backslash-newline continuations preserve keyword eligibility.
|
|
68
|
+
keywordEligible = false;
|
|
69
|
+
constructor(owner = null) {
|
|
70
|
+
this._owner = owner;
|
|
71
|
+
}
|
|
72
|
+
get value() {
|
|
73
|
+
return (this._value ?? (this._value = this._owner === null ? "" : this._owner._tokenValue(this.pos, this.end, this.raw)));
|
|
74
|
+
}
|
|
75
|
+
set value(v) {
|
|
76
|
+
this._value = v;
|
|
77
|
+
}
|
|
78
|
+
reset() {
|
|
79
|
+
this.token = Token.EOF;
|
|
80
|
+
this._value = "";
|
|
81
|
+
this.pos = 0;
|
|
82
|
+
this.end = 0;
|
|
83
|
+
this.fileDescriptor = undefined;
|
|
84
|
+
this.variableName = undefined;
|
|
85
|
+
this.descriptorEnd = 0;
|
|
86
|
+
this.delimiterQuoted = false;
|
|
87
|
+
this.content = undefined;
|
|
88
|
+
this.targetPos = 0;
|
|
89
|
+
this.targetEnd = 0;
|
|
90
|
+
this.assignmentOperatorPos = -1;
|
|
91
|
+
this.raw = false;
|
|
92
|
+
this.keywordEligible = false;
|
|
93
|
+
}
|
|
94
|
+
copyFrom(other) {
|
|
95
|
+
this.token = other.token;
|
|
96
|
+
this._value = other._value;
|
|
97
|
+
this.pos = other.pos;
|
|
98
|
+
this.end = other.end;
|
|
99
|
+
this.fileDescriptor = other.fileDescriptor;
|
|
100
|
+
this.variableName = other.variableName;
|
|
101
|
+
this.descriptorEnd = other.descriptorEnd;
|
|
102
|
+
this.delimiterQuoted = other.delimiterQuoted;
|
|
103
|
+
this.content = other.content;
|
|
104
|
+
this.targetPos = other.targetPos;
|
|
105
|
+
this.targetEnd = other.targetEnd;
|
|
106
|
+
this.assignmentOperatorPos = other.assignmentOperatorPos;
|
|
107
|
+
this.raw = other.raw;
|
|
108
|
+
this.keywordEligible = other.keywordEligible;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const RESERVED_WORDS = new Map([
|
|
112
|
+
["if", Token.If],
|
|
113
|
+
["then", Token.Then],
|
|
114
|
+
["else", Token.Else],
|
|
115
|
+
["elif", Token.Elif],
|
|
116
|
+
["fi", Token.Fi],
|
|
117
|
+
["do", Token.Do],
|
|
118
|
+
["done", Token.Done],
|
|
119
|
+
["for", Token.For],
|
|
120
|
+
["while", Token.While],
|
|
121
|
+
["until", Token.Until],
|
|
122
|
+
["in", Token.In],
|
|
123
|
+
["case", Token.Case],
|
|
124
|
+
["esac", Token.Esac],
|
|
125
|
+
["function", Token.Function],
|
|
126
|
+
["select", Token.Select],
|
|
127
|
+
["coproc", Token.Coproc],
|
|
128
|
+
["!", Token.Bang],
|
|
129
|
+
["{", Token.LBrace],
|
|
130
|
+
["}", Token.RBrace],
|
|
131
|
+
]);
|
|
132
|
+
// `name()` is a syntax error for these; `time`, `[[` and `]]` are matched positionally
|
|
133
|
+
// rather than through RESERVED_WORDS, and a name spelled like an assignment reads as a prefix.
|
|
134
|
+
export function requiresFunctionKeyword(name) {
|
|
135
|
+
return RESERVED_WORDS.has(name) || name === "time" || name === "[[" || name === "]]" || name.includes("=");
|
|
136
|
+
}
|
|
137
|
+
// Combined character type table — bit 0: metachar, bit 1: word-special
|
|
138
|
+
const charType = new Uint8Array(128);
|
|
139
|
+
charType[CH_PIPE] = 1;
|
|
140
|
+
charType[CH_AMP] = 1;
|
|
141
|
+
charType[CH_SEMI] = 1;
|
|
142
|
+
charType[CH_LPAREN] = 1;
|
|
143
|
+
charType[CH_RPAREN] = 1;
|
|
144
|
+
charType[CH_LT] = 1;
|
|
145
|
+
charType[CH_GT] = 1;
|
|
146
|
+
charType[CH_SPACE] = 1;
|
|
147
|
+
charType[CH_TAB] = 1;
|
|
148
|
+
charType[CH_NL] = 1;
|
|
149
|
+
charType[CH_BACKSLASH] = 2;
|
|
150
|
+
charType[CH_SQUOTE] = 2;
|
|
151
|
+
charType[CH_DQUOTE] = 2;
|
|
152
|
+
charType[CH_DOLLAR] = 2;
|
|
153
|
+
charType[CH_BACKTICK] = 2;
|
|
154
|
+
charType[CH_LBRACE] = 2;
|
|
155
|
+
function skipBlanks(source, pos, end) {
|
|
156
|
+
while (pos < end) {
|
|
157
|
+
const ch = source.charCodeAt(pos);
|
|
158
|
+
if (ch === CH_SPACE || ch === CH_TAB)
|
|
159
|
+
pos++;
|
|
160
|
+
else if (ch === CH_BACKSLASH && pos + 1 < end && source.charCodeAt(pos + 1) === CH_NL)
|
|
161
|
+
pos += 2;
|
|
162
|
+
else
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
return pos;
|
|
166
|
+
}
|
|
167
|
+
const arithmeticWordDelimiter = new Uint8Array(128);
|
|
168
|
+
for (const ch of [
|
|
169
|
+
CH_TAB,
|
|
170
|
+
CH_NL,
|
|
171
|
+
CH_SPACE,
|
|
172
|
+
CH_BANG,
|
|
173
|
+
CH_PERCENT,
|
|
174
|
+
CH_AMP,
|
|
175
|
+
CH_LPAREN,
|
|
176
|
+
CH_RPAREN,
|
|
177
|
+
CH_STAR,
|
|
178
|
+
CH_PLUS,
|
|
179
|
+
CH_COMMA,
|
|
180
|
+
CH_DASH,
|
|
181
|
+
CH_SLASH,
|
|
182
|
+
CH_COLON,
|
|
183
|
+
CH_LT,
|
|
184
|
+
CH_EQ,
|
|
185
|
+
CH_GT,
|
|
186
|
+
CH_QUESTION,
|
|
187
|
+
CH_CARET,
|
|
188
|
+
CH_PIPE,
|
|
189
|
+
]) {
|
|
190
|
+
arithmeticWordDelimiter[ch] = 1;
|
|
191
|
+
}
|
|
192
|
+
export function hasEmbeddedWordStructure(source, start, end) {
|
|
193
|
+
for (let pos = start; pos < end; pos++) {
|
|
194
|
+
const ch = source.charCodeAt(pos);
|
|
195
|
+
if (ch === CH_BACKSLASH ||
|
|
196
|
+
ch === CH_SQUOTE ||
|
|
197
|
+
ch === CH_DQUOTE ||
|
|
198
|
+
ch === CH_DOLLAR ||
|
|
199
|
+
ch === CH_BACKTICK ||
|
|
200
|
+
((ch === CH_LT || ch === CH_GT) && pos + 1 < end && source.charCodeAt(pos + 1) === CH_LPAREN)) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
function findUnnested(s, target, pairTernaries = false, findNestedEnd) {
|
|
207
|
+
let depth = 0;
|
|
208
|
+
let ternaryDepth = 0;
|
|
209
|
+
for (let i = 0; i < s.length; i++) {
|
|
210
|
+
const c = s.charCodeAt(i);
|
|
211
|
+
if (c === CH_BACKSLASH) {
|
|
212
|
+
i++;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (c === CH_LBRACE) {
|
|
216
|
+
depth++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (c === CH_RBRACE) {
|
|
220
|
+
if (depth > 0)
|
|
221
|
+
depth--;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (c === CH_SQUOTE) {
|
|
225
|
+
i++;
|
|
226
|
+
while (i < s.length && s.charCodeAt(i) !== CH_SQUOTE)
|
|
227
|
+
i++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (c === CH_DQUOTE) {
|
|
231
|
+
i++;
|
|
232
|
+
while (i < s.length) {
|
|
233
|
+
const quoted = s.charCodeAt(i);
|
|
234
|
+
if (quoted === CH_DQUOTE)
|
|
235
|
+
break;
|
|
236
|
+
if (quoted === CH_BACKSLASH) {
|
|
237
|
+
i += 2;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const nestedEnd = findNestedEnd?.(i, true) ?? i;
|
|
241
|
+
if (nestedEnd > i) {
|
|
242
|
+
i = nestedEnd;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
i++;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const nestedEnd = findNestedEnd?.(i, false) ?? i;
|
|
250
|
+
if (nestedEnd > i) {
|
|
251
|
+
i = nestedEnd - 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (pairTernaries && depth === 0) {
|
|
255
|
+
if (c === CH_QUESTION) {
|
|
256
|
+
ternaryDepth++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (c === CH_COLON && ternaryDepth > 0) {
|
|
260
|
+
ternaryDepth--;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (c === target && depth === 0)
|
|
265
|
+
return i;
|
|
266
|
+
}
|
|
267
|
+
return -1;
|
|
268
|
+
}
|
|
269
|
+
// Lookup: identifier chars (a-z, A-Z, 0-9, _) — bit 0: start, bit 1: continue
|
|
270
|
+
const isIdChar = new Uint8Array(128);
|
|
271
|
+
for (let i = CH_a; i <= CH_z; i++)
|
|
272
|
+
isIdChar[i] = 3;
|
|
273
|
+
for (let i = CH_A; i <= CH_Z; i++)
|
|
274
|
+
isIdChar[i] = 3;
|
|
275
|
+
for (let i = CH_0; i <= CH_9; i++)
|
|
276
|
+
isIdChar[i] = 2;
|
|
277
|
+
isIdChar[CH_UNDERSCORE] = 3;
|
|
278
|
+
const extglobPrefix = new Uint8Array(128);
|
|
279
|
+
extglobPrefix[CH_QUESTION] = 1;
|
|
280
|
+
extglobPrefix[CH_AT] = 1;
|
|
281
|
+
extglobPrefix[CH_STAR] = 1;
|
|
282
|
+
extglobPrefix[CH_PLUS] = 1;
|
|
283
|
+
extglobPrefix[CH_BANG] = 1;
|
|
284
|
+
extglobPrefix[CH_EQ] = 1;
|
|
285
|
+
// `=(` opens an array value, not a pattern.
|
|
286
|
+
function isExtglobOpen(prefix) {
|
|
287
|
+
return prefix < 128 && prefix !== CH_EQ && extglobPrefix[prefix] === 1;
|
|
288
|
+
}
|
|
289
|
+
const extglobOp = {
|
|
290
|
+
[CH_QUESTION]: "?",
|
|
291
|
+
[CH_AT]: "@",
|
|
292
|
+
[CH_STAR]: "*",
|
|
293
|
+
[CH_PLUS]: "+",
|
|
294
|
+
[CH_BANG]: "!",
|
|
295
|
+
};
|
|
296
|
+
function isDQChild(p) {
|
|
297
|
+
const t = p.type;
|
|
298
|
+
return (t === "Literal" ||
|
|
299
|
+
t === "SimpleExpansion" ||
|
|
300
|
+
t === "ParameterExpansion" ||
|
|
301
|
+
t === "CommandExpansion" ||
|
|
302
|
+
t === "ArithmeticExpansion");
|
|
303
|
+
}
|
|
304
|
+
function isAllDigitsRange(src, start, end) {
|
|
305
|
+
for (let i = start; i < end; i++) {
|
|
306
|
+
const c = src.charCodeAt(i);
|
|
307
|
+
if (c < CH_0 || c > CH_9)
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return end > start;
|
|
311
|
+
}
|
|
312
|
+
const ASSIGNMENT_INVALID = -1;
|
|
313
|
+
const ASSIGNMENT_NAME_START = 0;
|
|
314
|
+
const ASSIGNMENT_NAME = 1;
|
|
315
|
+
const ASSIGNMENT_AFTER_INDEX = 2;
|
|
316
|
+
const ASSIGNMENT_AFTER_PLUS = 3;
|
|
317
|
+
const ASSIGNMENT_INDEX_BASE = 4;
|
|
318
|
+
function joinWordParts(head, tail) {
|
|
319
|
+
const last = head[head.length - 1];
|
|
320
|
+
const first = tail[0];
|
|
321
|
+
let start = 0;
|
|
322
|
+
if (last?.type === "Literal" && first?.type === "Literal" && last.end === first.pos) {
|
|
323
|
+
head[head.length - 1] = {
|
|
324
|
+
type: "Literal",
|
|
325
|
+
pos: last.pos,
|
|
326
|
+
end: first.end,
|
|
327
|
+
text: last.text + first.text,
|
|
328
|
+
value: last.value + first.value,
|
|
329
|
+
};
|
|
330
|
+
start = 1;
|
|
331
|
+
}
|
|
332
|
+
for (let i = start; i < tail.length; i++)
|
|
333
|
+
head.push(tail[i]);
|
|
334
|
+
return head;
|
|
335
|
+
}
|
|
336
|
+
function isMatchedAssignment(state) {
|
|
337
|
+
return state < ASSIGNMENT_INVALID;
|
|
338
|
+
}
|
|
339
|
+
function assignmentOperatorPos(state) {
|
|
340
|
+
return -state - 2;
|
|
341
|
+
}
|
|
342
|
+
function scanAssignmentPrefix(src, start, end, initialState) {
|
|
343
|
+
let state = initialState;
|
|
344
|
+
for (let i = start; i < end && state >= 0; i++) {
|
|
345
|
+
const c = src.charCodeAt(i);
|
|
346
|
+
if (state >= ASSIGNMENT_INDEX_BASE) {
|
|
347
|
+
if (c === CH_LBRACKET)
|
|
348
|
+
state++;
|
|
349
|
+
else if (c === CH_RBRACKET && --state === ASSIGNMENT_INDEX_BASE)
|
|
350
|
+
state = ASSIGNMENT_AFTER_INDEX;
|
|
351
|
+
}
|
|
352
|
+
else if (state === ASSIGNMENT_NAME_START) {
|
|
353
|
+
state = c < 128 && isIdChar[c] & 1 ? ASSIGNMENT_NAME : ASSIGNMENT_INVALID;
|
|
354
|
+
}
|
|
355
|
+
else if (state === ASSIGNMENT_NAME) {
|
|
356
|
+
if (c < 128 && isIdChar[c] & 2)
|
|
357
|
+
continue;
|
|
358
|
+
if (c === CH_LBRACKET)
|
|
359
|
+
state = ASSIGNMENT_INDEX_BASE + 1;
|
|
360
|
+
else if (c === CH_PLUS)
|
|
361
|
+
state = ASSIGNMENT_AFTER_PLUS;
|
|
362
|
+
else
|
|
363
|
+
state = c === CH_EQ ? -i - 2 : ASSIGNMENT_INVALID;
|
|
364
|
+
}
|
|
365
|
+
else if (state === ASSIGNMENT_AFTER_INDEX) {
|
|
366
|
+
if (c === CH_PLUS)
|
|
367
|
+
state = ASSIGNMENT_AFTER_PLUS;
|
|
368
|
+
else
|
|
369
|
+
state = c === CH_EQ ? -i - 2 : ASSIGNMENT_INVALID;
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
state = c === CH_EQ ? -i - 2 : ASSIGNMENT_INVALID;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return state;
|
|
376
|
+
}
|
|
377
|
+
function setToken(out, token, value, pos = 0, end = 0) {
|
|
378
|
+
out.token = token;
|
|
379
|
+
out._value = value;
|
|
380
|
+
out.pos = pos;
|
|
381
|
+
out.end = end;
|
|
382
|
+
out.fileDescriptor = undefined;
|
|
383
|
+
out.variableName = undefined;
|
|
384
|
+
out.descriptorEnd = 0;
|
|
385
|
+
out.delimiterQuoted = false;
|
|
386
|
+
out.content = undefined;
|
|
387
|
+
out.assignmentOperatorPos = -1;
|
|
388
|
+
out.raw = false;
|
|
389
|
+
out.keywordEligible = false;
|
|
390
|
+
}
|
|
391
|
+
// Word token over [pos, end) whose value materializes on first access.
|
|
392
|
+
function setSpanToken(out, token, pos, end, raw) {
|
|
393
|
+
out.token = token;
|
|
394
|
+
out._value = null;
|
|
395
|
+
out.pos = pos;
|
|
396
|
+
out.end = end;
|
|
397
|
+
out.fileDescriptor = undefined;
|
|
398
|
+
out.variableName = undefined;
|
|
399
|
+
out.descriptorEnd = 0;
|
|
400
|
+
out.delimiterQuoted = false;
|
|
401
|
+
out.content = undefined;
|
|
402
|
+
out.assignmentOperatorPos = -1;
|
|
403
|
+
out.raw = raw;
|
|
404
|
+
out.keywordEligible = false;
|
|
405
|
+
}
|
|
406
|
+
// Positional reserved-word match over src[start, start+len) — no slice. Mirrors
|
|
407
|
+
// RESERVED_WORDS; first-char dispatch plus length checks reject fast.
|
|
408
|
+
function matchReservedWord(src, start, len) {
|
|
409
|
+
switch (src.charCodeAt(start)) {
|
|
410
|
+
case CH_BANG:
|
|
411
|
+
return len === 1 ? Token.Bang : undefined;
|
|
412
|
+
case CH_LBRACE:
|
|
413
|
+
return len === 1 ? Token.LBrace : undefined;
|
|
414
|
+
case CH_RBRACE:
|
|
415
|
+
return len === 1 ? Token.RBrace : undefined;
|
|
416
|
+
case 0x69 /* i */: {
|
|
417
|
+
if (len !== 2)
|
|
418
|
+
return undefined;
|
|
419
|
+
const c = src.charCodeAt(start + 1);
|
|
420
|
+
return c === 0x66 /* f */ ? Token.If : c === 0x6e /* n */ ? Token.In : undefined;
|
|
421
|
+
}
|
|
422
|
+
case 0x66 /* f */:
|
|
423
|
+
if (len === 2)
|
|
424
|
+
return src.charCodeAt(start + 1) === 0x69 /* i */ ? Token.Fi : undefined;
|
|
425
|
+
if (len === 3)
|
|
426
|
+
return src.startsWith("for", start) ? Token.For : undefined;
|
|
427
|
+
if (len === 8)
|
|
428
|
+
return src.startsWith("function", start) ? Token.Function : undefined;
|
|
429
|
+
return undefined;
|
|
430
|
+
case 0x74 /* t */:
|
|
431
|
+
return len === 4 && src.startsWith("then", start) ? Token.Then : undefined;
|
|
432
|
+
case 0x65 /* e */:
|
|
433
|
+
if (len !== 4)
|
|
434
|
+
return undefined;
|
|
435
|
+
if (src.startsWith("else", start))
|
|
436
|
+
return Token.Else;
|
|
437
|
+
if (src.startsWith("elif", start))
|
|
438
|
+
return Token.Elif;
|
|
439
|
+
if (src.startsWith("esac", start))
|
|
440
|
+
return Token.Esac;
|
|
441
|
+
return undefined;
|
|
442
|
+
case 0x64 /* d */:
|
|
443
|
+
if (len === 2)
|
|
444
|
+
return src.charCodeAt(start + 1) === 0x6f /* o */ ? Token.Do : undefined;
|
|
445
|
+
if (len === 4)
|
|
446
|
+
return src.startsWith("done", start) ? Token.Done : undefined;
|
|
447
|
+
return undefined;
|
|
448
|
+
case 0x63 /* c */:
|
|
449
|
+
if (len === 4)
|
|
450
|
+
return src.startsWith("case", start) ? Token.Case : undefined;
|
|
451
|
+
if (len === 6)
|
|
452
|
+
return src.startsWith("coproc", start) ? Token.Coproc : undefined;
|
|
453
|
+
return undefined;
|
|
454
|
+
case 0x77 /* w */:
|
|
455
|
+
return len === 5 && src.startsWith("while", start) ? Token.While : undefined;
|
|
456
|
+
case 0x75 /* u */:
|
|
457
|
+
return len === 5 && src.startsWith("until", start) ? Token.Until : undefined;
|
|
458
|
+
case 0x73 /* s */:
|
|
459
|
+
return len === 6 && src.startsWith("select", start) ? Token.Select : undefined;
|
|
460
|
+
default:
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
export const LexContext = {
|
|
465
|
+
Normal: 0,
|
|
466
|
+
CommandStart: 1,
|
|
467
|
+
TestMode: 2,
|
|
468
|
+
// After a prefix element: assignments still recognized, reserved words not.
|
|
469
|
+
CommandPrefix: 3,
|
|
470
|
+
// Assignment arguments retain ordinary word boundaries inside subscripts.
|
|
471
|
+
Declaration: 4,
|
|
472
|
+
ArrayElement: 5,
|
|
473
|
+
FunctionName: 6,
|
|
474
|
+
CasePattern: 7,
|
|
475
|
+
// Arguments of eval and let: ordinary words, but `name=(…)` keeps its array value as in Bash.
|
|
476
|
+
EvalArgument: 8,
|
|
477
|
+
};
|
|
478
|
+
export class Lexer {
|
|
479
|
+
static _findSubstitutionEnd;
|
|
480
|
+
src;
|
|
481
|
+
srcEnd;
|
|
482
|
+
parenBoundary;
|
|
483
|
+
pos;
|
|
484
|
+
current;
|
|
485
|
+
nextState;
|
|
486
|
+
hasPeek;
|
|
487
|
+
pendingHereDocs;
|
|
488
|
+
substitutionContext;
|
|
489
|
+
_errors = null;
|
|
490
|
+
_buildParts = false;
|
|
491
|
+
// Build processed text while scanning. Off on the normal token path (values
|
|
492
|
+
// materialize lazily); on for redirect targets, heredoc delimiters, arithmetic
|
|
493
|
+
// command bodies, and bounded value re-lexes. _buildParts implies it.
|
|
494
|
+
_buildValue = false;
|
|
495
|
+
// Nesting depth of the window being lexed (enclosing sub-fields plus substitution
|
|
496
|
+
// scripts), sharing the MAX_SYNTAX_NESTING budget across lazily created lexers.
|
|
497
|
+
_nestingDepth = 0;
|
|
498
|
+
// `start`/`end` bound the lexer to a window of `src` so substitution scripts can be
|
|
499
|
+
// parsed in place against the original source — every position is then absolute, with
|
|
500
|
+
// no slicing or re-basing. Defaults cover the whole string (the common top-level parse).
|
|
501
|
+
constructor(src, start = 0, end = src.length, parenBoundary = false, context = createSubstitutionContext()) {
|
|
502
|
+
this.src = src;
|
|
503
|
+
this.srcEnd = end;
|
|
504
|
+
this.parenBoundary = parenBoundary;
|
|
505
|
+
this.pos = start;
|
|
506
|
+
this.current = new TokenValue(this);
|
|
507
|
+
this.nextState = new TokenValue(this);
|
|
508
|
+
this.hasPeek = false;
|
|
509
|
+
this.pendingHereDocs = null;
|
|
510
|
+
this.substitutionContext = context;
|
|
511
|
+
if (start === 0 && src.charCodeAt(0) === CH_HASH && src.charCodeAt(1) === CH_BANG) {
|
|
512
|
+
const nl = src.indexOf("\n");
|
|
513
|
+
this.pos = nl === -1 ? this.srcEnd : nl + 1;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
getSource() {
|
|
517
|
+
return this.src;
|
|
518
|
+
}
|
|
519
|
+
// Fresh window sharing source, depth and substitution context; tokens, errors, heredocs and build flags reset.
|
|
520
|
+
fork(start, end, parenBoundary = false) {
|
|
521
|
+
const lexer = new Lexer(this.src, start, end, parenBoundary, this.substitutionContext);
|
|
522
|
+
lexer._nestingDepth = this._nestingDepth;
|
|
523
|
+
return lexer;
|
|
524
|
+
}
|
|
525
|
+
getSubstitutionContext() {
|
|
526
|
+
return this.substitutionContext;
|
|
527
|
+
}
|
|
528
|
+
get errors() {
|
|
529
|
+
return this._errors ?? (this._errors = []);
|
|
530
|
+
}
|
|
531
|
+
// A substitution's script parses lazily, one level below the window it was found in, in
|
|
532
|
+
// place over `inner` at `start`, sharing this scan's substitution boundaries.
|
|
533
|
+
commandExpansion(pos, text, start, inner, parenBoundary = false) {
|
|
534
|
+
const window = this.scriptWindow(this.src, start, start + inner.length, parenBoundary);
|
|
535
|
+
return new ScriptExpansionImpl("CommandExpansion", pos, this.pos, text, window);
|
|
536
|
+
}
|
|
537
|
+
// Escaped backticks rebuild `inner` with the escapes removed, so it is no longer a verbatim
|
|
538
|
+
// substring of the source: that script parses the decoded body as its own source.
|
|
539
|
+
decodedCommandExpansion(pos, text, inner) {
|
|
540
|
+
const window = this.scriptWindow(inner, 0, inner.length, false, true);
|
|
541
|
+
return new ScriptExpansionImpl("CommandExpansion", pos, this.pos, text, window);
|
|
542
|
+
}
|
|
543
|
+
processSubstitution(pos, text, operator, inner) {
|
|
544
|
+
const window = this.scriptWindow(this.src, pos + 2, pos + 2 + inner.length, !this._unbalanced);
|
|
545
|
+
return new ProcessSubstitutionImpl(pos, this.pos, text, operator, window);
|
|
546
|
+
}
|
|
547
|
+
// Past the nesting budget the part gets no window, so lazy materialization bottoms out.
|
|
548
|
+
scriptWindow(source, start, end, parenBoundary, decoded = false) {
|
|
549
|
+
if (this._nestingDepth > MAX_SYNTAX_NESTING)
|
|
550
|
+
return undefined;
|
|
551
|
+
const context = decoded ? undefined : this.substitutionContext;
|
|
552
|
+
return { source, start, end, depth: this._nestingDepth + 1, parenBoundary, context };
|
|
553
|
+
}
|
|
554
|
+
getPos() {
|
|
555
|
+
return this.pos;
|
|
556
|
+
}
|
|
557
|
+
findSubstitutionEnd(start, end, depth = this._nestingDepth + 1) {
|
|
558
|
+
if (Lexer._findSubstitutionEnd === undefined)
|
|
559
|
+
return -1;
|
|
560
|
+
return Lexer._findSubstitutionEnd(this.src, start, end, depth, this.substitutionContext);
|
|
561
|
+
}
|
|
562
|
+
/** Materialize a word token's value: raw spans slice directly, others re-lex the span. */
|
|
563
|
+
_tokenValue(pos, end, raw) {
|
|
564
|
+
return raw ? this.src.slice(pos, end) : this.wordValueOf(pos, end);
|
|
565
|
+
}
|
|
566
|
+
// Re-lex [start, end) in value mode to produce the processed word text. Bounded
|
|
567
|
+
// to the span, so every expansion the original scan consumed closes within it.
|
|
568
|
+
// Errors were already reported by the original scan; suppress duplicates.
|
|
569
|
+
wordValueOf(start, end) {
|
|
570
|
+
const savedPos = this.pos;
|
|
571
|
+
const savedEnd = this.srcEnd;
|
|
572
|
+
const savedBuildValue = this._buildValue;
|
|
573
|
+
const savedUnbalanced = this._unbalanced;
|
|
574
|
+
const errorCount = this._errors === null ? 0 : this._errors.length;
|
|
575
|
+
this.pos = start;
|
|
576
|
+
this.srcEnd = end;
|
|
577
|
+
this._buildValue = true;
|
|
578
|
+
this.readWordText();
|
|
579
|
+
if (this.pos < end &&
|
|
580
|
+
this._wordAssignmentState !== undefined &&
|
|
581
|
+
this._wordAssignmentState >= ASSIGNMENT_INDEX_BASE) {
|
|
582
|
+
this.pos = start;
|
|
583
|
+
this.readWordText(true);
|
|
584
|
+
}
|
|
585
|
+
const value = this._wordText;
|
|
586
|
+
this.pos = savedPos;
|
|
587
|
+
this.srcEnd = savedEnd;
|
|
588
|
+
this._buildValue = savedBuildValue;
|
|
589
|
+
this._unbalanced = savedUnbalanced;
|
|
590
|
+
if (this._errors !== null)
|
|
591
|
+
this._errors.length = errorCount;
|
|
592
|
+
return value;
|
|
593
|
+
}
|
|
594
|
+
/** Find the closing bracket for a shell subscript, ignoring brackets inside nested shell syntax. */
|
|
595
|
+
findClosingBracket(start, end = this.srcEnd) {
|
|
596
|
+
// A failed search scans to the end and every unclosed subscript at a command position
|
|
597
|
+
// retries it, so answer from the last bracket in the window first.
|
|
598
|
+
const limit = Math.min(end, this.srcEnd);
|
|
599
|
+
let window = this._bracketWindow;
|
|
600
|
+
if (!window || window.limit !== limit) {
|
|
601
|
+
window = { limit, last: this.src.lastIndexOf("]", limit - 1) };
|
|
602
|
+
this._bracketWindow = window;
|
|
603
|
+
}
|
|
604
|
+
if (start > window.last)
|
|
605
|
+
return -1;
|
|
606
|
+
return this.findClosingShellDelimiter(start, end, CH_RBRACKET);
|
|
607
|
+
}
|
|
608
|
+
/** Find the closing bracket of `$[ … ]`, which unlike a subscript does not recurse into `${ }`. */
|
|
609
|
+
findClosingArithmeticBracket(start, end = this.srcEnd) {
|
|
610
|
+
return this.findClosingShellDelimiter(start, end, CH_RBRACKET, false, false);
|
|
611
|
+
}
|
|
612
|
+
/** Find the closing brace for a parameter expansion, ignoring braces inside nested shell syntax. */
|
|
613
|
+
findClosingBrace(start, end = this.srcEnd) {
|
|
614
|
+
return this.findClosingShellDelimiter(start, end, CH_RBRACE);
|
|
615
|
+
}
|
|
616
|
+
/** Find the closing parenthesis for a shell substitution using the command-aware scanner. */
|
|
617
|
+
findClosingParenthesis(start, end = this.srcEnd) {
|
|
618
|
+
const savedPos = this.pos;
|
|
619
|
+
const savedEnd = this.srcEnd;
|
|
620
|
+
const savedUnbalanced = this._unbalanced;
|
|
621
|
+
this.pos = start;
|
|
622
|
+
this.srcEnd = Math.min(end, this.srcEnd);
|
|
623
|
+
this.extractBalanced();
|
|
624
|
+
const close = this._unbalanced ? -1 : this.pos - 1;
|
|
625
|
+
this.pos = savedPos;
|
|
626
|
+
this.srcEnd = savedEnd;
|
|
627
|
+
this._unbalanced = savedUnbalanced;
|
|
628
|
+
return close;
|
|
629
|
+
}
|
|
630
|
+
/** Find the end of one arithmetic expansion using the canonical lexer scanner. */
|
|
631
|
+
findArithmeticExpansionEnd(start, end = this.srcEnd) {
|
|
632
|
+
const scanner = this.fork(start, end);
|
|
633
|
+
scanner.pos = start + 1;
|
|
634
|
+
scanner.scanArithmeticBody();
|
|
635
|
+
return scanner.pos;
|
|
636
|
+
}
|
|
637
|
+
/** Find the end of one shell-expanded arithmetic word using the canonical lexer scanners. */
|
|
638
|
+
findArithmeticWordEnd(start, end = this.srcEnd) {
|
|
639
|
+
const scanner = this.fork(start, end);
|
|
640
|
+
scanner.pos = start;
|
|
641
|
+
return scanner.scanArithmeticWordEnd();
|
|
642
|
+
}
|
|
643
|
+
scanArithmeticWordEnd() {
|
|
644
|
+
while (this.pos < this.srcEnd) {
|
|
645
|
+
const ch = this.src.charCodeAt(this.pos);
|
|
646
|
+
if (ch === CH_DOLLAR) {
|
|
647
|
+
this.readDollar();
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (ch === CH_BACKTICK) {
|
|
651
|
+
this.readBacktickExpansion();
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (ch === CH_SQUOTE) {
|
|
655
|
+
this.pos++;
|
|
656
|
+
this.skipSQ();
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (ch === CH_DQUOTE) {
|
|
660
|
+
this.pos++;
|
|
661
|
+
this.skipDQ();
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (ch === CH_BACKSLASH) {
|
|
665
|
+
this.pos += 2;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (ch === CH_LBRACKET) {
|
|
669
|
+
const close = this.findClosingBracket(this.pos + 1);
|
|
670
|
+
if (close !== -1) {
|
|
671
|
+
this.pos = close + 1;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if ((ch === CH_LT || ch === CH_GT) && this.src.charCodeAt(this.pos + 1) === CH_LPAREN) {
|
|
676
|
+
this.pos += 2;
|
|
677
|
+
this.extractBalanced();
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (ch < 128 && arithmeticWordDelimiter[ch])
|
|
681
|
+
break;
|
|
682
|
+
this.pos++;
|
|
683
|
+
}
|
|
684
|
+
return this.pos;
|
|
685
|
+
}
|
|
686
|
+
// Only array assignment bodies take comments and leading `[` subscripts and reject operators;
|
|
687
|
+
// extglob shares this scanner, and `#` is pattern data there.
|
|
688
|
+
findClosingShellDelimiter(start, end, closing, array = false, braces = true) {
|
|
689
|
+
const savedPos = this.pos;
|
|
690
|
+
const savedEnd = this.srcEnd;
|
|
691
|
+
const savedUnbalanced = this._unbalanced;
|
|
692
|
+
this.srcEnd = Math.min(end, this.srcEnd);
|
|
693
|
+
const delimiters = [closing];
|
|
694
|
+
let pos = start;
|
|
695
|
+
let wordStart = true;
|
|
696
|
+
let invalid = -1;
|
|
697
|
+
while (pos < this.srcEnd) {
|
|
698
|
+
const ch = this.src.charCodeAt(pos);
|
|
699
|
+
if (ch === CH_BACKSLASH) {
|
|
700
|
+
if (pos + 1 < this.srcEnd && this.src.charCodeAt(pos + 1) !== CH_NL)
|
|
701
|
+
wordStart = false;
|
|
702
|
+
pos += 2;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (ch === CH_HASH && array && delimiters.length === 1 && wordStart) {
|
|
706
|
+
while (pos < this.srcEnd && this.src.charCodeAt(pos) !== CH_NL)
|
|
707
|
+
pos++;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (ch === CH_SQUOTE) {
|
|
711
|
+
this.pos = pos + 1;
|
|
712
|
+
this.skipSQ();
|
|
713
|
+
pos = this.pos;
|
|
714
|
+
wordStart = false;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (ch === CH_DQUOTE) {
|
|
718
|
+
this.pos = pos + 1;
|
|
719
|
+
this.skipDQ();
|
|
720
|
+
pos = this.pos;
|
|
721
|
+
wordStart = false;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (ch === CH_BACKTICK) {
|
|
725
|
+
pos++;
|
|
726
|
+
while (pos < this.srcEnd && this.src.charCodeAt(pos) !== CH_BACKTICK) {
|
|
727
|
+
if (this.src.charCodeAt(pos) === CH_BACKSLASH)
|
|
728
|
+
pos++;
|
|
729
|
+
pos++;
|
|
730
|
+
}
|
|
731
|
+
if (pos < this.srcEnd)
|
|
732
|
+
pos++;
|
|
733
|
+
wordStart = false;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if ((ch === CH_DOLLAR && pos + 1 < this.srcEnd && this.src.charCodeAt(pos + 1) === CH_LPAREN) ||
|
|
737
|
+
((ch === CH_LT || ch === CH_GT) && pos + 1 < this.srcEnd && this.src.charCodeAt(pos + 1) === CH_LPAREN)) {
|
|
738
|
+
this.pos = pos + 2;
|
|
739
|
+
this.extractBalanced();
|
|
740
|
+
pos = this.pos;
|
|
741
|
+
wordStart = false;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const expected = delimiters[delimiters.length - 1];
|
|
745
|
+
if (ch === CH_DOLLAR && pos + 1 < this.srcEnd) {
|
|
746
|
+
const after = this.src.charCodeAt(pos + 1);
|
|
747
|
+
if (after === CH_SQUOTE) {
|
|
748
|
+
pos = decodeAnsiCQuoted(this.src, pos + 2, this.srcEnd).end;
|
|
749
|
+
wordStart = false;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (after === CH_DOLLAR) {
|
|
753
|
+
pos += 2; // `$$` consumes its second `$`, so a `{` after it opens nothing
|
|
754
|
+
wordStart = false;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (after === CH_LBRACE && braces) {
|
|
758
|
+
delimiters.push(CH_RBRACE);
|
|
759
|
+
pos += 2;
|
|
760
|
+
wordStart = false;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (array && delimiters.length === 1) {
|
|
765
|
+
if (ch === CH_LBRACKET && wordStart) {
|
|
766
|
+
delimiters.push(CH_RBRACKET);
|
|
767
|
+
}
|
|
768
|
+
else if (invalid === -1 &&
|
|
769
|
+
(ch === CH_SEMI ||
|
|
770
|
+
ch === CH_AMP ||
|
|
771
|
+
ch === CH_PIPE ||
|
|
772
|
+
ch === CH_LT ||
|
|
773
|
+
ch === CH_GT ||
|
|
774
|
+
(ch === CH_LPAREN && !isExtglobOpen(this.src.charCodeAt(pos - 1))))) {
|
|
775
|
+
invalid = pos;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (expected === CH_RBRACKET && ch === CH_LBRACKET) {
|
|
779
|
+
delimiters.push(CH_RBRACKET);
|
|
780
|
+
}
|
|
781
|
+
else if (expected === CH_RPAREN && ch === CH_LPAREN) {
|
|
782
|
+
delimiters.push(CH_RPAREN);
|
|
783
|
+
}
|
|
784
|
+
else if (ch === expected) {
|
|
785
|
+
delimiters.pop();
|
|
786
|
+
if (delimiters.length === 0) {
|
|
787
|
+
this.pos = savedPos;
|
|
788
|
+
this.srcEnd = savedEnd;
|
|
789
|
+
this._unbalanced = savedUnbalanced;
|
|
790
|
+
if (invalid !== -1)
|
|
791
|
+
this.errors.push({ message: `unexpected '${this.src[invalid]}' in array assignment`, pos: invalid });
|
|
792
|
+
return pos;
|
|
793
|
+
}
|
|
794
|
+
wordStart = false;
|
|
795
|
+
pos++;
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
wordStart = ch < 128 && (charType[ch] & 1) !== 0;
|
|
799
|
+
pos++;
|
|
800
|
+
}
|
|
801
|
+
this.pos = savedPos;
|
|
802
|
+
this.srcEnd = savedEnd;
|
|
803
|
+
this._unbalanced = savedUnbalanced;
|
|
804
|
+
return -1;
|
|
805
|
+
}
|
|
806
|
+
// End of the brace expansion opened at `start`, or -1. As in Bash's brace_gobbler, quotes,
|
|
807
|
+
// escapes, parameter expansions and substitutions are opaque; an unquoted metacharacter
|
|
808
|
+
// ends the word before the brace closes. The word scan reports errors in the skipped text.
|
|
809
|
+
scanBraceExpansion(start) {
|
|
810
|
+
const src = this.src;
|
|
811
|
+
const len = this.srcEnd;
|
|
812
|
+
const first = start + 1 < len ? src.charCodeAt(start + 1) : 0;
|
|
813
|
+
if (first <= CH_SPACE || first === CH_RBRACE)
|
|
814
|
+
return -1;
|
|
815
|
+
const savedPos = this.pos;
|
|
816
|
+
const savedUnbalanced = this._unbalanced;
|
|
817
|
+
const errorCount = this._errors === null ? 0 : this._errors.length;
|
|
818
|
+
let depth = 1;
|
|
819
|
+
let hasSep = false;
|
|
820
|
+
let end = -1;
|
|
821
|
+
let pos = start + 1;
|
|
822
|
+
while (pos < len) {
|
|
823
|
+
const ch = src.charCodeAt(pos);
|
|
824
|
+
const type = ch < 128 ? charType[ch] : 0;
|
|
825
|
+
if (type === 0) {
|
|
826
|
+
pos++;
|
|
827
|
+
if (ch === CH_RBRACE) {
|
|
828
|
+
if (--depth === 0) {
|
|
829
|
+
end = pos;
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
else if (depth === 1 && (ch === 0x2c /* , */ || (ch === 0x2e /* . */ && src.charCodeAt(pos) === 0x2e))) {
|
|
834
|
+
hasSep = true;
|
|
835
|
+
}
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const next = pos + 1 < len ? src.charCodeAt(pos + 1) : 0;
|
|
839
|
+
if (type & 1) {
|
|
840
|
+
if (next === CH_LPAREN && (ch === CH_LT || ch === CH_GT)) {
|
|
841
|
+
this.pos = pos + 2;
|
|
842
|
+
this.extractBalanced();
|
|
843
|
+
pos = this.pos;
|
|
844
|
+
}
|
|
845
|
+
else if (ch === CH_LPAREN && isExtglobOpen(src.charCodeAt(pos - 1))) {
|
|
846
|
+
const close = this.findClosingShellDelimiter(pos + 1, len, CH_RPAREN);
|
|
847
|
+
if (close === -1)
|
|
848
|
+
break;
|
|
849
|
+
pos = close + 1;
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
else if (ch === CH_BACKSLASH) {
|
|
856
|
+
pos += 2;
|
|
857
|
+
}
|
|
858
|
+
else if (ch === CH_SQUOTE) {
|
|
859
|
+
this.pos = pos + 1;
|
|
860
|
+
this.skipSQ();
|
|
861
|
+
pos = this.pos;
|
|
862
|
+
}
|
|
863
|
+
else if (ch === CH_DQUOTE) {
|
|
864
|
+
this.pos = pos + 1;
|
|
865
|
+
this.skipDQ();
|
|
866
|
+
pos = this.pos;
|
|
867
|
+
}
|
|
868
|
+
else if (ch === CH_BACKTICK) {
|
|
869
|
+
pos++;
|
|
870
|
+
while (pos < len && src.charCodeAt(pos) !== CH_BACKTICK)
|
|
871
|
+
pos += src.charCodeAt(pos) === CH_BACKSLASH ? 2 : 1;
|
|
872
|
+
pos++;
|
|
873
|
+
}
|
|
874
|
+
else if (ch === CH_LBRACE) {
|
|
875
|
+
depth++;
|
|
876
|
+
pos++;
|
|
877
|
+
}
|
|
878
|
+
else if (next === CH_SQUOTE) {
|
|
879
|
+
pos = decodeAnsiCQuoted(src, pos + 2, len).end;
|
|
880
|
+
}
|
|
881
|
+
else if (next === CH_LBRACE) {
|
|
882
|
+
const close = this.findClosingBrace(pos + 2, len);
|
|
883
|
+
if (close === -1)
|
|
884
|
+
break;
|
|
885
|
+
pos = close + 1;
|
|
886
|
+
}
|
|
887
|
+
else if (next === CH_LPAREN) {
|
|
888
|
+
this.pos = pos + 2;
|
|
889
|
+
this.extractBalanced();
|
|
890
|
+
pos = this.pos;
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
pos++;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
this.pos = savedPos;
|
|
897
|
+
this._unbalanced = savedUnbalanced;
|
|
898
|
+
if (this._errors !== null)
|
|
899
|
+
this._errors.length = errorCount;
|
|
900
|
+
return hasSep ? end : -1;
|
|
901
|
+
}
|
|
902
|
+
skipSubshellBody() {
|
|
903
|
+
this.extractBalanced(false);
|
|
904
|
+
return this._unbalanced ? -1 : this.pos;
|
|
905
|
+
}
|
|
906
|
+
skipCompoundBody(closeToken) {
|
|
907
|
+
const frames = [
|
|
908
|
+
{ close: closeToken, phase: closeToken === Token.Esac ? "case-pattern" : "commands" },
|
|
909
|
+
];
|
|
910
|
+
let commandStart = true;
|
|
911
|
+
for (;;) {
|
|
912
|
+
const phase = frames[frames.length - 1].phase;
|
|
913
|
+
let ctx = commandStart ? LexContext.CommandStart : LexContext.Normal;
|
|
914
|
+
if (phase === "function-name")
|
|
915
|
+
ctx = LexContext.FunctionName;
|
|
916
|
+
else if (phase === "case-pattern" && commandStart)
|
|
917
|
+
ctx = LexContext.CasePattern;
|
|
918
|
+
const value = this.next(ctx);
|
|
919
|
+
const token = value.token;
|
|
920
|
+
if (token === Token.EOF)
|
|
921
|
+
return -1;
|
|
922
|
+
const last = frames.length - 1;
|
|
923
|
+
const frame = frames[last];
|
|
924
|
+
if (frame.phase === "function-name") {
|
|
925
|
+
if (token === Token.Newline)
|
|
926
|
+
continue;
|
|
927
|
+
frame.phase = "function-body";
|
|
928
|
+
commandStart = true;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
else if (frame.phase === "function-body") {
|
|
932
|
+
if (token === Token.Newline)
|
|
933
|
+
continue;
|
|
934
|
+
frame.phase = "commands";
|
|
935
|
+
commandStart = true;
|
|
936
|
+
if (token === Token.LParen && this.peek(LexContext.Normal).token === Token.RParen) {
|
|
937
|
+
this.next(LexContext.Normal);
|
|
938
|
+
frame.phase = "function-body";
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
else if (frame.phase === "coproc-command") {
|
|
943
|
+
if (token === Token.Newline)
|
|
944
|
+
continue;
|
|
945
|
+
if (token === Token.Word) {
|
|
946
|
+
frame.phase = "coproc-body";
|
|
947
|
+
commandStart = true;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
frame.phase = "commands";
|
|
951
|
+
commandStart = true;
|
|
952
|
+
}
|
|
953
|
+
else if (frame.phase === "coproc-body") {
|
|
954
|
+
if (token === Token.Newline)
|
|
955
|
+
continue;
|
|
956
|
+
frame.phase =
|
|
957
|
+
token === Token.Word && value.keywordEligible && value.value === "time" ? "time-command" : "commands";
|
|
958
|
+
commandStart = true;
|
|
959
|
+
if (frame.phase === "time-command")
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
else if (frame.phase === "time-command") {
|
|
963
|
+
if (token === Token.Word && value.keywordEligible && value.value === "-p") {
|
|
964
|
+
frame.phase = "time-command-after-p";
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (token === Token.Word && value.keywordEligible && value.value === "--") {
|
|
968
|
+
frame.phase = "commands";
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
frame.phase = "commands";
|
|
972
|
+
commandStart = true;
|
|
973
|
+
}
|
|
974
|
+
else if (frame.phase === "time-command-after-p") {
|
|
975
|
+
if (token === Token.Word && value.keywordEligible && value.value === "--") {
|
|
976
|
+
frame.phase = "commands";
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
frame.phase = "commands";
|
|
980
|
+
commandStart = true;
|
|
981
|
+
}
|
|
982
|
+
else if (frame.phase === "for-header") {
|
|
983
|
+
if (token === Token.ArithCmd || token === Token.Semi || token === Token.Newline) {
|
|
984
|
+
commandStart = true;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (token === Token.Do || token === Token.LBrace) {
|
|
988
|
+
frame.close = token === Token.Do ? Token.Done : Token.RBrace;
|
|
989
|
+
frame.phase = "commands";
|
|
990
|
+
commandStart = true;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
else if (frame.phase === "case-word") {
|
|
995
|
+
if (token === Token.Newline)
|
|
996
|
+
continue;
|
|
997
|
+
frame.phase = "case-in";
|
|
998
|
+
commandStart = false;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
else if (frame.phase === "case-in") {
|
|
1002
|
+
if (token === Token.Newline) {
|
|
1003
|
+
commandStart = true;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
frame.phase = "case-pattern";
|
|
1007
|
+
commandStart = true;
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
else if (frame.phase === "case-pattern") {
|
|
1011
|
+
if (token === Token.Esac && commandStart) {
|
|
1012
|
+
frames.pop();
|
|
1013
|
+
if (frames.length === 0)
|
|
1014
|
+
return value.end;
|
|
1015
|
+
commandStart = false;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (token === Token.RParen) {
|
|
1019
|
+
frame.phase = "commands";
|
|
1020
|
+
commandStart = true;
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
commandStart = token === Token.Newline;
|
|
1024
|
+
}
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (token === frame.close) {
|
|
1028
|
+
frames.pop();
|
|
1029
|
+
if (frames.length === 0)
|
|
1030
|
+
return value.end;
|
|
1031
|
+
commandStart = false;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (commandStart) {
|
|
1035
|
+
switch (token) {
|
|
1036
|
+
case Token.LParen:
|
|
1037
|
+
frames.push({ close: Token.RParen, phase: "commands" });
|
|
1038
|
+
break;
|
|
1039
|
+
case Token.LBrace:
|
|
1040
|
+
frames.push({ close: Token.RBrace, phase: "commands" });
|
|
1041
|
+
break;
|
|
1042
|
+
case Token.If:
|
|
1043
|
+
frames.push({ close: Token.Fi, phase: "commands" });
|
|
1044
|
+
break;
|
|
1045
|
+
case Token.For:
|
|
1046
|
+
frames.push({ close: Token.Done, phase: "for-header" });
|
|
1047
|
+
break;
|
|
1048
|
+
case Token.While:
|
|
1049
|
+
case Token.Until:
|
|
1050
|
+
case Token.Select:
|
|
1051
|
+
frames.push({ close: Token.Done, phase: "commands" });
|
|
1052
|
+
break;
|
|
1053
|
+
case Token.Case:
|
|
1054
|
+
frames.push({ close: Token.Esac, phase: "case-word" });
|
|
1055
|
+
break;
|
|
1056
|
+
case Token.DblLBracket:
|
|
1057
|
+
if (!this.skipTestCommandBody())
|
|
1058
|
+
return -1;
|
|
1059
|
+
commandStart = false;
|
|
1060
|
+
continue;
|
|
1061
|
+
case Token.Assignment:
|
|
1062
|
+
case Token.Redirect:
|
|
1063
|
+
case Token.Bang:
|
|
1064
|
+
case Token.Then:
|
|
1065
|
+
case Token.Else:
|
|
1066
|
+
case Token.Elif:
|
|
1067
|
+
case Token.Do:
|
|
1068
|
+
case Token.In:
|
|
1069
|
+
break;
|
|
1070
|
+
case Token.Semi:
|
|
1071
|
+
case Token.Newline:
|
|
1072
|
+
case Token.Pipe:
|
|
1073
|
+
case Token.And:
|
|
1074
|
+
case Token.Or:
|
|
1075
|
+
case Token.Amp:
|
|
1076
|
+
case Token.DoubleSemi:
|
|
1077
|
+
case Token.SemiAmp:
|
|
1078
|
+
case Token.DoubleSemiAmp:
|
|
1079
|
+
break;
|
|
1080
|
+
case Token.Function:
|
|
1081
|
+
frame.phase = "function-name";
|
|
1082
|
+
break;
|
|
1083
|
+
case Token.Coproc:
|
|
1084
|
+
frame.phase = "coproc-command";
|
|
1085
|
+
break;
|
|
1086
|
+
default:
|
|
1087
|
+
if (token === Token.Word && value.keywordEligible && value.value === "time") {
|
|
1088
|
+
frame.phase = "time-command";
|
|
1089
|
+
commandStart = true;
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
commandStart = false;
|
|
1093
|
+
}
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
switch (token) {
|
|
1098
|
+
case Token.Semi:
|
|
1099
|
+
case Token.Newline:
|
|
1100
|
+
case Token.Pipe:
|
|
1101
|
+
case Token.And:
|
|
1102
|
+
case Token.Or:
|
|
1103
|
+
case Token.Amp:
|
|
1104
|
+
commandStart = true;
|
|
1105
|
+
break;
|
|
1106
|
+
case Token.DoubleSemi:
|
|
1107
|
+
case Token.SemiAmp:
|
|
1108
|
+
case Token.DoubleSemiAmp:
|
|
1109
|
+
if (frame.close === Token.Esac)
|
|
1110
|
+
frame.phase = "case-pattern";
|
|
1111
|
+
commandStart = true;
|
|
1112
|
+
break;
|
|
1113
|
+
case Token.RParen:
|
|
1114
|
+
commandStart = true;
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
skipTestGroup() {
|
|
1120
|
+
let depth = 1;
|
|
1121
|
+
for (;;) {
|
|
1122
|
+
const value = this.next(LexContext.TestMode);
|
|
1123
|
+
if (value.token === Token.EOF)
|
|
1124
|
+
return -1;
|
|
1125
|
+
if (value.token === Token.DblRBracket) {
|
|
1126
|
+
this.unshift(value);
|
|
1127
|
+
return -1;
|
|
1128
|
+
}
|
|
1129
|
+
if (value.token === Token.LParen)
|
|
1130
|
+
depth++;
|
|
1131
|
+
else if (value.token === Token.RParen && --depth === 0)
|
|
1132
|
+
return value.end;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
skipTestCommandBody() {
|
|
1136
|
+
for (;;) {
|
|
1137
|
+
const token = this.next(LexContext.TestMode).token;
|
|
1138
|
+
if (token === Token.DblRBracket)
|
|
1139
|
+
return true;
|
|
1140
|
+
if (token === Token.EOF)
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
/** Set position and scan a word, building parts. Used by computeWordParts. */
|
|
1145
|
+
buildWordParts(startPos) {
|
|
1146
|
+
this._buildParts = true;
|
|
1147
|
+
this.pos = startPos;
|
|
1148
|
+
// Handle process substitution words <(...) and >(...)
|
|
1149
|
+
const ch = this.src.charCodeAt(startPos);
|
|
1150
|
+
if ((ch === 0x3c /* < */ || ch === 0x3e) /* > */ &&
|
|
1151
|
+
startPos + 1 < this.srcEnd &&
|
|
1152
|
+
this.src.charCodeAt(startPos + 1) === 0x28 /* ( */) {
|
|
1153
|
+
this.pos = startPos + 2;
|
|
1154
|
+
const inner = this.extractBalanced();
|
|
1155
|
+
const text = this.src.slice(startPos, this.pos);
|
|
1156
|
+
const part = this.processSubstitution(startPos, text, ch === 0x3c ? "<" : ">", inner);
|
|
1157
|
+
// Continue reading any trailing word text (e.g., suffix after proc sub)
|
|
1158
|
+
if (this.pos < this.srcEnd) {
|
|
1159
|
+
this.readWordText();
|
|
1160
|
+
if (this._wordParts) {
|
|
1161
|
+
this._wordParts.unshift(part);
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
this._wordParts = [part];
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
this._wordParts = [part];
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
this.readWordText();
|
|
1173
|
+
// A command word whose subscript holds a blank was bounded by the matched-pair rescan, so
|
|
1174
|
+
// its parts need the same scan.
|
|
1175
|
+
if (this.pos < this.srcEnd &&
|
|
1176
|
+
this._wordAssignmentState !== undefined &&
|
|
1177
|
+
this._wordAssignmentState >= ASSIGNMENT_INDEX_BASE) {
|
|
1178
|
+
this.pos = startPos;
|
|
1179
|
+
this.readWordText(true);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return this._wordParts;
|
|
1183
|
+
}
|
|
1184
|
+
/** Scan a bounded word-like span without treating shell operators or whitespace as terminators. */
|
|
1185
|
+
buildEmbeddedWordParts(startPos, arithmetic = false) {
|
|
1186
|
+
this._buildParts = true;
|
|
1187
|
+
this.pos = startPos;
|
|
1188
|
+
this.readInnerWordText(false, arithmetic);
|
|
1189
|
+
return this._wordParts;
|
|
1190
|
+
}
|
|
1191
|
+
buildArrayElementParts(startPos) {
|
|
1192
|
+
const close = this.findClosingBracket(startPos + 1);
|
|
1193
|
+
if (close === -1)
|
|
1194
|
+
return this.buildWordParts(startPos);
|
|
1195
|
+
const src = this.src;
|
|
1196
|
+
this._buildParts = true;
|
|
1197
|
+
const head = hasEmbeddedWordStructure(src, startPos, close + 1)
|
|
1198
|
+
? this.parseSubFieldWord(startPos, close + 1, "embedded")
|
|
1199
|
+
: undefined;
|
|
1200
|
+
this.pos = close + 1;
|
|
1201
|
+
this.readWordText(false, LexContext.ArrayElement);
|
|
1202
|
+
const end = this.pos;
|
|
1203
|
+
// Only `[key]=` and `[key]+=` select an element; its key is never a pattern. Any other element
|
|
1204
|
+
// is a value that undergoes pathname expansion, so one glob pass owns the whole element and a
|
|
1205
|
+
// leading bracket is a pattern as in a command word.
|
|
1206
|
+
const operator = skipLineContinuations(src, close + 1, end);
|
|
1207
|
+
const after = src.charCodeAt(operator);
|
|
1208
|
+
const keyed = after === CH_EQ || (after === CH_PLUS && src.charCodeAt(operator + 1) === CH_EQ);
|
|
1209
|
+
let headParts = head?.parts;
|
|
1210
|
+
let tailParts = this._wordParts;
|
|
1211
|
+
if (keyed)
|
|
1212
|
+
tailParts = parseGlobParts(src, close + 1, end, tailParts) ?? tailParts;
|
|
1213
|
+
if (!headParts && !tailParts)
|
|
1214
|
+
return keyed ? null : (parseGlobParts(src, startPos, end, undefined) ?? null);
|
|
1215
|
+
const headText = src.slice(startPos, close + 1);
|
|
1216
|
+
if (!headParts)
|
|
1217
|
+
headParts = [{ type: "Literal", pos: startPos, end: close + 1, text: headText, value: head?.value ?? headText }];
|
|
1218
|
+
if (!tailParts)
|
|
1219
|
+
tailParts =
|
|
1220
|
+
end > close + 1
|
|
1221
|
+
? [{ type: "Literal", pos: close + 1, end, text: src.slice(close + 1, end), value: this._wordText }]
|
|
1222
|
+
: [];
|
|
1223
|
+
let parts = joinWordParts(headParts, tailParts);
|
|
1224
|
+
if (!keyed)
|
|
1225
|
+
parts = parseGlobParts(src, startPos, end, parts) ?? parts;
|
|
1226
|
+
return parts.length > 1 || parts[0].type !== "Literal" ? parts : null;
|
|
1227
|
+
}
|
|
1228
|
+
/** Scan a heredoc body for expansions, building parts. Spaces/newlines are literal. */
|
|
1229
|
+
buildHereDocParts(bodyPos, bodyEnd) {
|
|
1230
|
+
this._buildParts = true;
|
|
1231
|
+
const src = this.src;
|
|
1232
|
+
const parts = [];
|
|
1233
|
+
let hasExpansion = false;
|
|
1234
|
+
let litBuf = "";
|
|
1235
|
+
let litStart = bodyPos;
|
|
1236
|
+
let i = bodyPos;
|
|
1237
|
+
const flushLit = () => {
|
|
1238
|
+
if (i > litStart) {
|
|
1239
|
+
parts.push({ type: "Literal", pos: litStart, end: i, value: litBuf, text: src.slice(litStart, i) });
|
|
1240
|
+
litBuf = "";
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
while (i < bodyEnd) {
|
|
1244
|
+
const ch = src.charCodeAt(i);
|
|
1245
|
+
if (ch === 0x5c /* \\ */) {
|
|
1246
|
+
// Backslash escape — in unquoted heredoc, \\$, \\`, \\\\ are special
|
|
1247
|
+
if (i + 1 < bodyEnd) {
|
|
1248
|
+
const nc = src.charCodeAt(i + 1);
|
|
1249
|
+
if (nc === CH_NL) {
|
|
1250
|
+
i += 2;
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (nc === 0x24 /* $ */ || nc === 0x60 /* ` */ || nc === 0x5c /* \\ */) {
|
|
1254
|
+
litBuf += String.fromCharCode(nc);
|
|
1255
|
+
i += 2;
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
litBuf += "\\";
|
|
1260
|
+
i++;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (ch === 0x24 /* $ */ && src.charCodeAt(i + 1) !== CH_SQUOTE && src.charCodeAt(i + 1) !== CH_DQUOTE) {
|
|
1264
|
+
flushLit();
|
|
1265
|
+
litStart = i;
|
|
1266
|
+
this.pos = i;
|
|
1267
|
+
this.readDollar();
|
|
1268
|
+
if (this._resultPart) {
|
|
1269
|
+
hasExpansion = true;
|
|
1270
|
+
parts.push(this._resultPart);
|
|
1271
|
+
litStart = this.pos;
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
litBuf += src.slice(i, this.pos);
|
|
1275
|
+
}
|
|
1276
|
+
i = this.pos;
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (ch === 0x60 /* ` */) {
|
|
1280
|
+
flushLit();
|
|
1281
|
+
litStart = i;
|
|
1282
|
+
this.pos = i;
|
|
1283
|
+
this.readBacktickExpansion();
|
|
1284
|
+
if (this._resultPart) {
|
|
1285
|
+
hasExpansion = true;
|
|
1286
|
+
parts.push(this._resultPart);
|
|
1287
|
+
litStart = this.pos;
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
litBuf += src.slice(i, this.pos);
|
|
1291
|
+
}
|
|
1292
|
+
i = this.pos;
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
litBuf += src[i];
|
|
1296
|
+
i++;
|
|
1297
|
+
}
|
|
1298
|
+
flushLit();
|
|
1299
|
+
return hasExpansion ? parts : null;
|
|
1300
|
+
}
|
|
1301
|
+
registerHereDocTarget(target) {
|
|
1302
|
+
if (this.pendingHereDocs === null)
|
|
1303
|
+
return;
|
|
1304
|
+
for (const hd of this.pendingHereDocs) {
|
|
1305
|
+
if (!hd.target) {
|
|
1306
|
+
hd.target = target;
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
// Read the right-hand operand of =~ in [[ ]]. Bash ends the operand at
|
|
1312
|
+
// depth-zero whitespace, `)`, `;`, `&`, `<`, or `>` (but `<(`/`>(` open a
|
|
1313
|
+
// process substitution and `|` never delimits). An unquoted `(` opens a
|
|
1314
|
+
// group that consumes everything — `]]`, newlines, and metacharacters
|
|
1315
|
+
// included — until its matching `)`; inside a group only quotes stay opaque
|
|
1316
|
+
// and expansion parens count naively, matching bash. Quotes and expansions
|
|
1317
|
+
// at depth zero skip via the same readers normal words use, so their errors
|
|
1318
|
+
// and spans stay identical. The token is the raw source span; value and
|
|
1319
|
+
// parts resolve lazily like every other word.
|
|
1320
|
+
readTestRegexWord() {
|
|
1321
|
+
this.hasPeek = false;
|
|
1322
|
+
this.skipSpacesAndTabs();
|
|
1323
|
+
const src = this.src;
|
|
1324
|
+
const len = this.srcEnd;
|
|
1325
|
+
const start = this.pos;
|
|
1326
|
+
let depth = 0;
|
|
1327
|
+
while (this.pos < len) {
|
|
1328
|
+
const ch = src.charCodeAt(this.pos);
|
|
1329
|
+
if (ch === CH_LPAREN) {
|
|
1330
|
+
depth++;
|
|
1331
|
+
this.pos++;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (ch === CH_BACKSLASH) {
|
|
1335
|
+
this.pos += this.pos + 1 < len ? 2 : 1;
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (ch === CH_SQUOTE) {
|
|
1339
|
+
const quotePos = this.pos++;
|
|
1340
|
+
const ansiC = quotePos > start && src.charCodeAt(quotePos - 1) === CH_DOLLAR;
|
|
1341
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_SQUOTE) {
|
|
1342
|
+
if (ansiC && src.charCodeAt(this.pos) === CH_BACKSLASH && this.pos + 1 < len)
|
|
1343
|
+
this.pos++;
|
|
1344
|
+
this.pos++;
|
|
1345
|
+
}
|
|
1346
|
+
if (this.pos < len)
|
|
1347
|
+
this.pos++;
|
|
1348
|
+
else
|
|
1349
|
+
this.errors.push({
|
|
1350
|
+
message: ansiC ? "unterminated ANSI-C quote" : "unterminated single quote",
|
|
1351
|
+
pos: quotePos,
|
|
1352
|
+
});
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
if (ch === CH_DQUOTE) {
|
|
1356
|
+
this.pos++;
|
|
1357
|
+
this.readDoubleQuoted();
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
if (ch === CH_BACKTICK) {
|
|
1361
|
+
this.readBacktickExpansion();
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
if (depth > 0) {
|
|
1365
|
+
if (ch === CH_RPAREN)
|
|
1366
|
+
depth--;
|
|
1367
|
+
this.pos++;
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (ch === CH_DOLLAR) {
|
|
1371
|
+
this.readDollar();
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
if ((ch === CH_LT || ch === CH_GT) && this.pos + 1 < len && src.charCodeAt(this.pos + 1) === CH_LPAREN) {
|
|
1375
|
+
const subPos = this.pos;
|
|
1376
|
+
this.pos += 2;
|
|
1377
|
+
this.extractBalanced();
|
|
1378
|
+
if (this._unbalanced)
|
|
1379
|
+
this.errors.push({ message: "unterminated process substitution", pos: subPos });
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (ch < 128 && charType[ch] & 1 && ch !== CH_PIPE)
|
|
1383
|
+
break;
|
|
1384
|
+
this.pos++;
|
|
1385
|
+
}
|
|
1386
|
+
setToken(this.current, Token.Word, src.slice(start, this.pos), start, this.pos);
|
|
1387
|
+
return this.current;
|
|
1388
|
+
}
|
|
1389
|
+
// Read C-style for expressions: called after first '(' consumed by parser.
|
|
1390
|
+
// Expects pos at second '('. Returns [init, test, update] raw text.
|
|
1391
|
+
readCStyleForExprs() {
|
|
1392
|
+
this.hasPeek = false; // discard any peeked token
|
|
1393
|
+
const src = this.src;
|
|
1394
|
+
const len = this.srcEnd;
|
|
1395
|
+
// Skip spaces to second '('
|
|
1396
|
+
while (this.pos < len && (src.charCodeAt(this.pos) === CH_SPACE || src.charCodeAt(this.pos) === CH_TAB))
|
|
1397
|
+
this.pos++;
|
|
1398
|
+
const openPos = this.pos;
|
|
1399
|
+
if (this.pos < len && src.charCodeAt(this.pos) === CH_LPAREN)
|
|
1400
|
+
this.pos++;
|
|
1401
|
+
const starts = [this.pos, 0, 0];
|
|
1402
|
+
const parts = ["", "", "", 0, 0, 0];
|
|
1403
|
+
let partIdx = 0;
|
|
1404
|
+
let depth = 1;
|
|
1405
|
+
let partStart = this.pos;
|
|
1406
|
+
let extra = false;
|
|
1407
|
+
const setPart = (end) => {
|
|
1408
|
+
const raw = src.slice(partStart, end);
|
|
1409
|
+
parts[partIdx] = raw.trim();
|
|
1410
|
+
parts[3 + partIdx] = starts[partIdx] + raw.length - raw.trimStart().length;
|
|
1411
|
+
};
|
|
1412
|
+
while (this.pos < len && depth > 0) {
|
|
1413
|
+
const c = src.charCodeAt(this.pos);
|
|
1414
|
+
if (c === CH_LPAREN) {
|
|
1415
|
+
depth++;
|
|
1416
|
+
this.pos++;
|
|
1417
|
+
}
|
|
1418
|
+
else if (c === CH_RPAREN) {
|
|
1419
|
+
depth--;
|
|
1420
|
+
if (depth === 0) {
|
|
1421
|
+
if (!extra)
|
|
1422
|
+
setPart(this.pos);
|
|
1423
|
+
if (partIdx < 2)
|
|
1424
|
+
this.errors.push({ message: "expected three arithmetic expressions in for header", pos: this.pos });
|
|
1425
|
+
this.pos++; // skip closing )
|
|
1426
|
+
// Skip the outer ) as well
|
|
1427
|
+
while (this.pos < len && (src.charCodeAt(this.pos) === CH_SPACE || src.charCodeAt(this.pos) === CH_TAB))
|
|
1428
|
+
this.pos++;
|
|
1429
|
+
if (this.pos < len && src.charCodeAt(this.pos) === CH_RPAREN)
|
|
1430
|
+
this.pos++;
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
this.pos++;
|
|
1434
|
+
}
|
|
1435
|
+
else if (c === CH_SEMI && depth === 1) {
|
|
1436
|
+
// Bash requires exactly three expressions; keep the first three and report the rest.
|
|
1437
|
+
if (partIdx === 2) {
|
|
1438
|
+
if (!extra) {
|
|
1439
|
+
setPart(this.pos);
|
|
1440
|
+
this.errors.push({ message: "expected three arithmetic expressions in for header", pos: this.pos });
|
|
1441
|
+
}
|
|
1442
|
+
extra = true;
|
|
1443
|
+
this.pos++;
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
setPart(this.pos);
|
|
1447
|
+
partIdx++;
|
|
1448
|
+
this.pos++;
|
|
1449
|
+
partStart = this.pos;
|
|
1450
|
+
starts[partIdx] = partStart;
|
|
1451
|
+
}
|
|
1452
|
+
else if (c === CH_DOLLAR) {
|
|
1453
|
+
this.readDollar();
|
|
1454
|
+
}
|
|
1455
|
+
else if (c === CH_BACKTICK) {
|
|
1456
|
+
this.readBacktickExpansion();
|
|
1457
|
+
}
|
|
1458
|
+
else if (c === CH_BACKSLASH) {
|
|
1459
|
+
this.pos += this.pos + 1 < len ? 2 : 1;
|
|
1460
|
+
}
|
|
1461
|
+
else if (c === CH_SQUOTE) {
|
|
1462
|
+
this.pos++;
|
|
1463
|
+
this.skipSQ();
|
|
1464
|
+
}
|
|
1465
|
+
else if (c === CH_DQUOTE) {
|
|
1466
|
+
this.pos++;
|
|
1467
|
+
this.skipDQ();
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1470
|
+
this.pos++;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (depth > 0) {
|
|
1474
|
+
if (!extra)
|
|
1475
|
+
setPart(this.pos);
|
|
1476
|
+
this.errors.push({ message: "unterminated arithmetic for header", pos: openPos });
|
|
1477
|
+
}
|
|
1478
|
+
return parts;
|
|
1479
|
+
}
|
|
1480
|
+
peek(ctx = LexContext.Normal) {
|
|
1481
|
+
if (!this.hasPeek) {
|
|
1482
|
+
this.readNext(this.nextState, ctx);
|
|
1483
|
+
this.hasPeek = true;
|
|
1484
|
+
}
|
|
1485
|
+
return this.nextState;
|
|
1486
|
+
}
|
|
1487
|
+
// Peek where the context depends on the token just consumed. peek() ignores its ctx once a
|
|
1488
|
+
// token is cached, so the caller's context must only be derived when a read actually happens.
|
|
1489
|
+
peekFollow(closers) {
|
|
1490
|
+
if (!this.hasPeek) {
|
|
1491
|
+
const ctx = closers[this.current.token] ? LexContext.CommandStart : LexContext.Normal;
|
|
1492
|
+
this.readNext(this.nextState, ctx);
|
|
1493
|
+
this.hasPeek = true;
|
|
1494
|
+
}
|
|
1495
|
+
return this.nextState;
|
|
1496
|
+
}
|
|
1497
|
+
next(ctx = LexContext.Normal) {
|
|
1498
|
+
if (this.hasPeek) {
|
|
1499
|
+
this.hasPeek = false;
|
|
1500
|
+
const temp = this.current;
|
|
1501
|
+
this.current = this.nextState;
|
|
1502
|
+
this.nextState = temp;
|
|
1503
|
+
return this.current;
|
|
1504
|
+
}
|
|
1505
|
+
this.readNext(this.current, ctx);
|
|
1506
|
+
return this.current;
|
|
1507
|
+
}
|
|
1508
|
+
unshift(tok) {
|
|
1509
|
+
this.nextState.copyFrom(tok);
|
|
1510
|
+
this.hasPeek = true;
|
|
1511
|
+
}
|
|
1512
|
+
readNext(out, ctx) {
|
|
1513
|
+
const src = this.src;
|
|
1514
|
+
const len = this.srcEnd;
|
|
1515
|
+
let pos = skipBlanks(src, this.pos, len);
|
|
1516
|
+
if (ctx === LexContext.TestMode) {
|
|
1517
|
+
while (pos < len && src.charCodeAt(pos) === CH_NL)
|
|
1518
|
+
pos = skipBlanks(src, pos + 1, len);
|
|
1519
|
+
}
|
|
1520
|
+
this.pos = pos;
|
|
1521
|
+
if (pos >= len) {
|
|
1522
|
+
this.consumePendingHereDocs();
|
|
1523
|
+
setToken(out, Token.EOF, "", pos, pos);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
const tokenStart = pos;
|
|
1527
|
+
const ch = src.charCodeAt(pos);
|
|
1528
|
+
if (ch === CH_HASH) {
|
|
1529
|
+
// Skip comment
|
|
1530
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_NL)
|
|
1531
|
+
this.pos++;
|
|
1532
|
+
this.readNext(out, ctx);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (ch === CH_NL) {
|
|
1536
|
+
this.pos++;
|
|
1537
|
+
this.consumePendingHereDocs();
|
|
1538
|
+
setToken(out, Token.Newline, "\n", tokenStart, this.pos);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
// In test mode (inside [[ ]]), < and > are string comparison operators, not redirects —
|
|
1542
|
+
// except for an adjacent '(', which still opens a process substitution
|
|
1543
|
+
if (ctx === LexContext.TestMode &&
|
|
1544
|
+
(ch === CH_LT || ch === CH_GT) &&
|
|
1545
|
+
!(this.pos + 1 < this.srcEnd && src.charCodeAt(this.pos + 1) === CH_LPAREN)) {
|
|
1546
|
+
this.pos++;
|
|
1547
|
+
setToken(out, Token.Word, ch === CH_LT ? "<" : ">", tokenStart, this.pos);
|
|
1548
|
+
out.keywordEligible = true; // written unquoted, so it is the comparison operator
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
// Operators all start with a metachar; anything else is a word
|
|
1552
|
+
if (ch < 128 && charType[ch] & 1 && this.tryReadOperator(out, ch, ctx, tokenStart))
|
|
1553
|
+
return;
|
|
1554
|
+
if (ch === CH_LBRACKET && ctx === LexContext.ArrayElement && this.readArrayElement(out, tokenStart))
|
|
1555
|
+
return;
|
|
1556
|
+
this.readWord(out, ctx, tokenStart);
|
|
1557
|
+
}
|
|
1558
|
+
tryReadOperator(out, ch, ctx, tokenStart) {
|
|
1559
|
+
const src = this.src;
|
|
1560
|
+
const pos = this.pos;
|
|
1561
|
+
const next = pos + 1 < this.srcEnd ? src.charCodeAt(pos + 1) : 0;
|
|
1562
|
+
switch (ch) {
|
|
1563
|
+
case CH_SEMI:
|
|
1564
|
+
if (next === CH_SEMI) {
|
|
1565
|
+
if (pos + 2 < this.srcEnd && src.charCodeAt(pos + 2) === CH_AMP) {
|
|
1566
|
+
this.pos += 3;
|
|
1567
|
+
setToken(out, Token.DoubleSemiAmp, ";;&", tokenStart, this.pos);
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
this.pos += 2;
|
|
1571
|
+
setToken(out, Token.DoubleSemi, ";;", tokenStart, this.pos);
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1574
|
+
if (next === CH_AMP) {
|
|
1575
|
+
this.pos += 2;
|
|
1576
|
+
setToken(out, Token.SemiAmp, ";&", tokenStart, this.pos);
|
|
1577
|
+
return true;
|
|
1578
|
+
}
|
|
1579
|
+
this.pos++;
|
|
1580
|
+
setToken(out, Token.Semi, ";", tokenStart, this.pos);
|
|
1581
|
+
return true;
|
|
1582
|
+
case CH_PIPE:
|
|
1583
|
+
if (next === CH_PIPE) {
|
|
1584
|
+
this.pos += 2;
|
|
1585
|
+
setToken(out, Token.Or, "||", tokenStart, this.pos);
|
|
1586
|
+
return true;
|
|
1587
|
+
}
|
|
1588
|
+
if (next === CH_AMP) {
|
|
1589
|
+
this.pos += 2;
|
|
1590
|
+
setToken(out, Token.Pipe, "|&", tokenStart, this.pos);
|
|
1591
|
+
return true;
|
|
1592
|
+
} // |& → pipe (stderr merge)
|
|
1593
|
+
this.pos++;
|
|
1594
|
+
setToken(out, Token.Pipe, "|", tokenStart, this.pos);
|
|
1595
|
+
return true;
|
|
1596
|
+
case CH_AMP:
|
|
1597
|
+
if (next === CH_AMP) {
|
|
1598
|
+
this.pos += 2;
|
|
1599
|
+
setToken(out, Token.And, "&&", tokenStart, this.pos);
|
|
1600
|
+
return true;
|
|
1601
|
+
}
|
|
1602
|
+
if (next === CH_GT) {
|
|
1603
|
+
// &> or &>> — redirect, not background
|
|
1604
|
+
this.pos += 2;
|
|
1605
|
+
const append = this.pos < this.srcEnd && src.charCodeAt(this.pos) === CH_GT;
|
|
1606
|
+
if (append)
|
|
1607
|
+
this.pos++;
|
|
1608
|
+
this.skipSpacesAndTabs();
|
|
1609
|
+
const targetPos = this.pos;
|
|
1610
|
+
if (this.pos < this.srcEnd && src.charCodeAt(this.pos) !== CH_NL && src.charCodeAt(this.pos) !== CH_HASH) {
|
|
1611
|
+
this.readRedirectTargetText();
|
|
1612
|
+
}
|
|
1613
|
+
this.redirectToken(out, append ? "&>>" : "&>", tokenStart, targetPos);
|
|
1614
|
+
return true;
|
|
1615
|
+
}
|
|
1616
|
+
this.pos++;
|
|
1617
|
+
setToken(out, Token.Amp, "&", tokenStart, this.pos);
|
|
1618
|
+
return true;
|
|
1619
|
+
case CH_LPAREN:
|
|
1620
|
+
if (ctx === LexContext.CommandStart && next === CH_LPAREN) {
|
|
1621
|
+
const savedErrors = this.errors.length;
|
|
1622
|
+
this.readArithmeticCommand(out, tokenStart);
|
|
1623
|
+
if (!this._notArithmetic)
|
|
1624
|
+
return true;
|
|
1625
|
+
this.errors.length = savedErrors;
|
|
1626
|
+
this.pos = tokenStart;
|
|
1627
|
+
}
|
|
1628
|
+
this.pos++;
|
|
1629
|
+
setToken(out, Token.LParen, "(", tokenStart, this.pos);
|
|
1630
|
+
return true;
|
|
1631
|
+
case CH_RPAREN:
|
|
1632
|
+
this.pos++;
|
|
1633
|
+
setToken(out, Token.RParen, ")", tokenStart, this.pos);
|
|
1634
|
+
return true;
|
|
1635
|
+
case CH_LT:
|
|
1636
|
+
case CH_GT:
|
|
1637
|
+
return this.readRedirection(out, tokenStart);
|
|
1638
|
+
default:
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
readRedirection(out, tokenStart) {
|
|
1643
|
+
const src = this.src;
|
|
1644
|
+
const ch = src.charCodeAt(this.pos);
|
|
1645
|
+
let op = "";
|
|
1646
|
+
if (ch === CH_LT) {
|
|
1647
|
+
this.pos++;
|
|
1648
|
+
const next = this.pos < this.srcEnd ? src.charCodeAt(this.pos) : 0;
|
|
1649
|
+
if (next === CH_LT) {
|
|
1650
|
+
this.pos++;
|
|
1651
|
+
const third = this.pos < this.srcEnd ? src.charCodeAt(this.pos) : 0;
|
|
1652
|
+
if (third === CH_LT) {
|
|
1653
|
+
// <<< herestring
|
|
1654
|
+
this.pos++;
|
|
1655
|
+
this.skipSpacesAndTabs();
|
|
1656
|
+
const targetPos = this.pos;
|
|
1657
|
+
if (this.pos < this.srcEnd && src.charCodeAt(this.pos) !== CH_NL && src.charCodeAt(this.pos) !== CH_HASH) {
|
|
1658
|
+
this.readRedirectTargetText();
|
|
1659
|
+
}
|
|
1660
|
+
this.redirectToken(out, "<<<", tokenStart, targetPos);
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
const dash = third === CH_DASH;
|
|
1664
|
+
if (dash)
|
|
1665
|
+
this.pos++;
|
|
1666
|
+
this.skipSpacesAndTabs();
|
|
1667
|
+
const targetPos = this.pos;
|
|
1668
|
+
if (this.pos >= this.srcEnd || src.charCodeAt(this.pos) !== CH_HASH)
|
|
1669
|
+
this.readHereDocDelimiter();
|
|
1670
|
+
const hasTarget = this.pos > targetPos;
|
|
1671
|
+
if (hasTarget) {
|
|
1672
|
+
(this.pendingHereDocs ?? (this.pendingHereDocs = [])).push({
|
|
1673
|
+
delimiter: this._hereDelim,
|
|
1674
|
+
strip: dash,
|
|
1675
|
+
quoted: this._hereQuoted,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
setToken(out, Token.Redirect, dash ? "<<-" : "<<", tokenStart, this.pos);
|
|
1679
|
+
out.content = hasTarget ? this._hereDelim : undefined;
|
|
1680
|
+
out.delimiterQuoted = hasTarget && this._hereQuoted;
|
|
1681
|
+
out.targetPos = targetPos;
|
|
1682
|
+
out.targetEnd = hasTarget ? this.pos : targetPos;
|
|
1683
|
+
return true;
|
|
1684
|
+
}
|
|
1685
|
+
if (next === CH_LPAREN) {
|
|
1686
|
+
this.readProcessSubstitution(out, "<", tokenStart);
|
|
1687
|
+
return true;
|
|
1688
|
+
}
|
|
1689
|
+
if (next === CH_GT) {
|
|
1690
|
+
op = "<>";
|
|
1691
|
+
this.pos++;
|
|
1692
|
+
}
|
|
1693
|
+
else if (next === CH_AMP) {
|
|
1694
|
+
op = "<&";
|
|
1695
|
+
this.pos++;
|
|
1696
|
+
}
|
|
1697
|
+
else {
|
|
1698
|
+
op = "<";
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
else if (ch === CH_GT) {
|
|
1702
|
+
this.pos++;
|
|
1703
|
+
const next = this.pos < this.srcEnd ? src.charCodeAt(this.pos) : 0;
|
|
1704
|
+
if (next === CH_LPAREN) {
|
|
1705
|
+
this.readProcessSubstitution(out, ">", tokenStart);
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
if (next === CH_GT) {
|
|
1709
|
+
op = ">>";
|
|
1710
|
+
this.pos++;
|
|
1711
|
+
}
|
|
1712
|
+
else if (next === CH_AMP) {
|
|
1713
|
+
op = ">&";
|
|
1714
|
+
this.pos++;
|
|
1715
|
+
}
|
|
1716
|
+
else if (next === CH_PIPE) {
|
|
1717
|
+
op = ">|";
|
|
1718
|
+
this.pos++;
|
|
1719
|
+
}
|
|
1720
|
+
else {
|
|
1721
|
+
op = ">";
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
this.skipSpacesAndTabs();
|
|
1725
|
+
if (this.pos < this.srcEnd) {
|
|
1726
|
+
const nc = src.charCodeAt(this.pos);
|
|
1727
|
+
if ((nc === CH_LT || nc === CH_GT) && this.pos + 1 < this.srcEnd && src.charCodeAt(this.pos + 1) === CH_LPAREN) {
|
|
1728
|
+
const psStart = this.pos;
|
|
1729
|
+
this.pos += 2;
|
|
1730
|
+
this.extractBalanced();
|
|
1731
|
+
if (this._unbalanced)
|
|
1732
|
+
this.errors.push({ message: "unterminated process substitution", pos: psStart });
|
|
1733
|
+
const psText = src.slice(psStart, this.pos);
|
|
1734
|
+
setToken(out, Token.Redirect, op, tokenStart, this.pos);
|
|
1735
|
+
out.content = psText;
|
|
1736
|
+
out.targetPos = psStart;
|
|
1737
|
+
out.targetEnd = this.pos;
|
|
1738
|
+
return true;
|
|
1739
|
+
}
|
|
1740
|
+
const targetPos = this.pos;
|
|
1741
|
+
if (nc !== CH_NL && nc !== CH_HASH)
|
|
1742
|
+
this.readRedirectTargetText();
|
|
1743
|
+
this.redirectToken(out, op, tokenStart, targetPos);
|
|
1744
|
+
return true;
|
|
1745
|
+
}
|
|
1746
|
+
this.redirectToken(out, op, tokenStart, this.pos);
|
|
1747
|
+
return true;
|
|
1748
|
+
}
|
|
1749
|
+
// Redirect targets keep their processed text eagerly (it becomes the target
|
|
1750
|
+
// word's content), so scan them in value mode.
|
|
1751
|
+
readRedirectTargetText() {
|
|
1752
|
+
const savedBuildValue = this._buildValue;
|
|
1753
|
+
this._buildValue = true;
|
|
1754
|
+
this.readWordText();
|
|
1755
|
+
this._buildValue = savedBuildValue;
|
|
1756
|
+
}
|
|
1757
|
+
redirectToken(out, operator, tokenStart, targetPos) {
|
|
1758
|
+
const hasTarget = this.pos > targetPos && (this._wordText.length > 0 || this._wordQuoted);
|
|
1759
|
+
setToken(out, Token.Redirect, operator, tokenStart, this.pos);
|
|
1760
|
+
out.content = hasTarget ? this._wordText : undefined;
|
|
1761
|
+
out.targetPos = targetPos;
|
|
1762
|
+
out.targetEnd = hasTarget ? this.pos : targetPos;
|
|
1763
|
+
}
|
|
1764
|
+
readProcessSubstitution(out, operator, tokenStart) {
|
|
1765
|
+
this.pos++; // skip (
|
|
1766
|
+
this.extractBalanced();
|
|
1767
|
+
if (this._unbalanced)
|
|
1768
|
+
this.errors.push({ message: "unterminated process substitution", pos: tokenStart });
|
|
1769
|
+
const text = this.src.slice(tokenStart, this.pos);
|
|
1770
|
+
setToken(out, Token.Word, text, tokenStart, this.pos);
|
|
1771
|
+
}
|
|
1772
|
+
// The delimiter is the word after quote removal: quote and escape segments may
|
|
1773
|
+
// appear anywhere in the word, any of them makes the heredoc quoted, and inside
|
|
1774
|
+
// double quotes a backslash is removed only before $ ` " \.
|
|
1775
|
+
readHereDocDelimiter() {
|
|
1776
|
+
const src = this.src;
|
|
1777
|
+
const len = this.srcEnd;
|
|
1778
|
+
const savedBuildValue = this._buildValue;
|
|
1779
|
+
this._buildValue = true; // the delimiter is assembled from processed segments
|
|
1780
|
+
let delimiter = "";
|
|
1781
|
+
let quoted = false;
|
|
1782
|
+
while (this.pos < len) {
|
|
1783
|
+
const c = src.charCodeAt(this.pos);
|
|
1784
|
+
if (c === CH_SQUOTE) {
|
|
1785
|
+
quoted = true;
|
|
1786
|
+
this.pos++;
|
|
1787
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_SQUOTE) {
|
|
1788
|
+
delimiter += src[this.pos];
|
|
1789
|
+
this.pos++;
|
|
1790
|
+
}
|
|
1791
|
+
if (this.pos < len)
|
|
1792
|
+
this.pos++;
|
|
1793
|
+
}
|
|
1794
|
+
else if (c === CH_DQUOTE) {
|
|
1795
|
+
quoted = true;
|
|
1796
|
+
this.pos++;
|
|
1797
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_DQUOTE) {
|
|
1798
|
+
if (src.charCodeAt(this.pos) === CH_BACKSLASH && this.pos + 1 < len) {
|
|
1799
|
+
const next = src.charCodeAt(this.pos + 1);
|
|
1800
|
+
if (next === CH_NL) {
|
|
1801
|
+
this.pos += 2;
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
if (next === CH_DOLLAR || next === CH_BACKTICK || next === CH_DQUOTE || next === CH_BACKSLASH)
|
|
1805
|
+
this.pos++;
|
|
1806
|
+
}
|
|
1807
|
+
delimiter += src[this.pos];
|
|
1808
|
+
this.pos++;
|
|
1809
|
+
}
|
|
1810
|
+
if (this.pos < len)
|
|
1811
|
+
this.pos++;
|
|
1812
|
+
}
|
|
1813
|
+
else if (c === CH_BACKSLASH) {
|
|
1814
|
+
if (this.pos + 1 < len && src.charCodeAt(this.pos + 1) === CH_NL) {
|
|
1815
|
+
this.pos += 2;
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
quoted = true;
|
|
1819
|
+
this.pos++;
|
|
1820
|
+
if (this.pos < len) {
|
|
1821
|
+
delimiter += src[this.pos];
|
|
1822
|
+
this.pos++;
|
|
1823
|
+
}
|
|
1824
|
+
else {
|
|
1825
|
+
delimiter += "\\"; // trailing backslash escapes nothing and stays as itself
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
else if (c === CH_BACKTICK) {
|
|
1829
|
+
const btStart = this.pos;
|
|
1830
|
+
this.pos++;
|
|
1831
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_BACKTICK) {
|
|
1832
|
+
if (src.charCodeAt(this.pos) === CH_BACKSLASH)
|
|
1833
|
+
this.pos++;
|
|
1834
|
+
this.pos++;
|
|
1835
|
+
}
|
|
1836
|
+
if (this.pos < len)
|
|
1837
|
+
this.pos++;
|
|
1838
|
+
delimiter += src.slice(btStart, this.pos);
|
|
1839
|
+
}
|
|
1840
|
+
else if (c === CH_DOLLAR) {
|
|
1841
|
+
const next = this.pos + 1 < len ? src.charCodeAt(this.pos + 1) : 0;
|
|
1842
|
+
if (next === CH_SQUOTE || next === CH_DQUOTE)
|
|
1843
|
+
quoted = true;
|
|
1844
|
+
this.readDollar();
|
|
1845
|
+
delimiter += this._resultText;
|
|
1846
|
+
}
|
|
1847
|
+
else if (c < 128 && charType[c] & 1) {
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
delimiter += src[this.pos];
|
|
1852
|
+
this.pos++;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
this._buildValue = savedBuildValue;
|
|
1856
|
+
this._hereDelim = delimiter;
|
|
1857
|
+
this._hereQuoted = quoted;
|
|
1858
|
+
}
|
|
1859
|
+
consumePendingHereDocs() {
|
|
1860
|
+
const pending = this.pendingHereDocs;
|
|
1861
|
+
if (pending === null || pending.length === 0)
|
|
1862
|
+
return;
|
|
1863
|
+
for (const hd of pending) {
|
|
1864
|
+
const bodyPos = this.pos;
|
|
1865
|
+
const bodyEnd = this.skipHereDocBody(hd.delimiter, hd.strip, this.parenBoundary, hd.quoted);
|
|
1866
|
+
if (hd.target) {
|
|
1867
|
+
hd.target.body.pos = bodyPos;
|
|
1868
|
+
hd.target.body.end = bodyEnd;
|
|
1869
|
+
hd.target.body.text = this.src.slice(bodyPos, bodyEnd);
|
|
1870
|
+
if (this.hereDocClosingPos !== -1)
|
|
1871
|
+
hd.target.closing = { pos: this.hereDocClosingPos, end: this.hereDocClosingEnd };
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
pending.length = 0;
|
|
1875
|
+
}
|
|
1876
|
+
hereDocClosingPos = -1;
|
|
1877
|
+
hereDocClosingEnd = -1;
|
|
1878
|
+
// Advance past the heredoc body and its delimiter line; return the body end
|
|
1879
|
+
// (start of the delimiter line, or srcEnd when delimited by end-of-input).
|
|
1880
|
+
// With parenEnds (inside $(...)), a line starting with the delimiter directly
|
|
1881
|
+
// followed by ")" also terminates the body, resuming at the ")" — bash treats
|
|
1882
|
+
// the substitution's closing paren as end-of-file for its heredocs.
|
|
1883
|
+
// `join` crosses `\`+newline pairs, which an unquoted delimiter may straddle.
|
|
1884
|
+
matchHereDocDelimiter(delimiter, lineStart, end, join) {
|
|
1885
|
+
const src = this.src;
|
|
1886
|
+
let pos = lineStart;
|
|
1887
|
+
for (let i = 0; i < delimiter.length;) {
|
|
1888
|
+
if (join && src.charCodeAt(pos) === CH_BACKSLASH && pos + 1 < end && src.charCodeAt(pos + 1) === CH_NL) {
|
|
1889
|
+
pos += 2;
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
if (pos >= end || src.charCodeAt(pos) !== delimiter.charCodeAt(i))
|
|
1893
|
+
return -1;
|
|
1894
|
+
pos++;
|
|
1895
|
+
i++;
|
|
1896
|
+
}
|
|
1897
|
+
return pos;
|
|
1898
|
+
}
|
|
1899
|
+
// A `\` consumes the next character, so `\`+newline continues the line and `\\` does not.
|
|
1900
|
+
logicalLineEnd(from, end, join) {
|
|
1901
|
+
const src = this.src;
|
|
1902
|
+
let lineEnd = src.indexOf("\n", from);
|
|
1903
|
+
while (lineEnd !== -1 && lineEnd < end) {
|
|
1904
|
+
if (!join)
|
|
1905
|
+
return lineEnd;
|
|
1906
|
+
let backslashes = lineEnd;
|
|
1907
|
+
while (backslashes > from && src.charCodeAt(backslashes - 1) === CH_BACKSLASH)
|
|
1908
|
+
backslashes--;
|
|
1909
|
+
if ((lineEnd - backslashes) % 2 === 0)
|
|
1910
|
+
return lineEnd;
|
|
1911
|
+
from = lineEnd + 1;
|
|
1912
|
+
lineEnd = src.indexOf("\n", from);
|
|
1913
|
+
}
|
|
1914
|
+
return end;
|
|
1915
|
+
}
|
|
1916
|
+
skipHereDocBody(delimiter, strip, parenEnds = false, quoted = false) {
|
|
1917
|
+
this.hereDocClosingPos = -1;
|
|
1918
|
+
this.hereDocClosingEnd = -1;
|
|
1919
|
+
const src = this.src;
|
|
1920
|
+
const len = this.srcEnd;
|
|
1921
|
+
while (this.pos < len) {
|
|
1922
|
+
let lineStart = this.pos;
|
|
1923
|
+
const lineEnd = this.logicalLineEnd(lineStart, len, !quoted);
|
|
1924
|
+
for (;;) {
|
|
1925
|
+
const before = lineStart;
|
|
1926
|
+
if (strip)
|
|
1927
|
+
while (lineStart < lineEnd && src.charCodeAt(lineStart) === CH_TAB)
|
|
1928
|
+
lineStart++;
|
|
1929
|
+
if (!quoted)
|
|
1930
|
+
lineStart = skipLineContinuations(src, lineStart, lineEnd);
|
|
1931
|
+
if (lineStart === before)
|
|
1932
|
+
break;
|
|
1933
|
+
}
|
|
1934
|
+
let afterDelim = this.matchHereDocDelimiter(delimiter, lineStart, lineEnd, !quoted);
|
|
1935
|
+
if (afterDelim !== -1 && !quoted)
|
|
1936
|
+
afterDelim = skipLineContinuations(src, afterDelim, lineEnd);
|
|
1937
|
+
if (afterDelim === lineEnd) {
|
|
1938
|
+
const bodyEnd = this.pos;
|
|
1939
|
+
if (!parenEnds || !this.parenBoundary || lineEnd !== len) {
|
|
1940
|
+
this.hereDocClosingPos = lineStart;
|
|
1941
|
+
this.hereDocClosingEnd = lineEnd;
|
|
1942
|
+
}
|
|
1943
|
+
this.pos = lineEnd < len ? lineEnd + 1 : lineEnd;
|
|
1944
|
+
return bodyEnd;
|
|
1945
|
+
}
|
|
1946
|
+
// A line starting with the delimiter also ends the body when a `)` follows on the same
|
|
1947
|
+
// logical line; the scan then resumes mid-line, right after the delimiter.
|
|
1948
|
+
if (parenEnds && afterDelim !== -1) {
|
|
1949
|
+
let paren = afterDelim;
|
|
1950
|
+
while (paren < lineEnd && src.charCodeAt(paren) !== CH_RPAREN)
|
|
1951
|
+
paren++;
|
|
1952
|
+
if (paren < lineEnd || (this.parenBoundary && lineEnd === len)) {
|
|
1953
|
+
const bodyEnd = this.pos;
|
|
1954
|
+
this.pos = afterDelim;
|
|
1955
|
+
return bodyEnd;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
this.pos = lineEnd < len ? lineEnd + 1 : lineEnd;
|
|
1959
|
+
}
|
|
1960
|
+
return this.pos;
|
|
1961
|
+
}
|
|
1962
|
+
_wordText = "";
|
|
1963
|
+
_wordRaw = false;
|
|
1964
|
+
_wordQuoted = false;
|
|
1965
|
+
_wordHasExpansions = false;
|
|
1966
|
+
_wordKeywordEligible = false;
|
|
1967
|
+
_wordAssignmentState;
|
|
1968
|
+
_bracketWindow;
|
|
1969
|
+
_wordParts = null;
|
|
1970
|
+
_resultText = "";
|
|
1971
|
+
// True when the last $-construct's value is exactly its raw source span.
|
|
1972
|
+
_resultIsRaw = true;
|
|
1973
|
+
_resultHasExpansion = false;
|
|
1974
|
+
_resultPart;
|
|
1975
|
+
// Set by extractBalanced when the input ended before the closing paren, so
|
|
1976
|
+
// callers can report the construct they were scanning.
|
|
1977
|
+
_unbalanced = false;
|
|
1978
|
+
// Set by scanArithmeticBody when the construct turns out not to be arithmetic, so the
|
|
1979
|
+
// caller can re-read it as a subshell or command substitution instead.
|
|
1980
|
+
_notArithmetic = false;
|
|
1981
|
+
_arithUnterminated = false;
|
|
1982
|
+
_dqText = "";
|
|
1983
|
+
_dqHasExpansions = false;
|
|
1984
|
+
_dqParts = null;
|
|
1985
|
+
// Content end (before the closing quote, or end of input when unterminated).
|
|
1986
|
+
_dqEnd = 0;
|
|
1987
|
+
_hereDelim = "";
|
|
1988
|
+
_hereQuoted = false;
|
|
1989
|
+
readWord(out, ctx, tokenStart = 0) {
|
|
1990
|
+
const errorStart = this._errors === null ? 0 : this._errors.length;
|
|
1991
|
+
this.readWordText(false, ctx, true);
|
|
1992
|
+
if (ctx === LexContext.Normal) {
|
|
1993
|
+
const next = this.pos < this.srcEnd ? this.src.charCodeAt(this.pos) : 0;
|
|
1994
|
+
if (!this._wordRaw || (next !== CH_LT && next !== CH_GT)) {
|
|
1995
|
+
setSpanToken(out, Token.Word, tokenStart, this.pos, this._wordRaw);
|
|
1996
|
+
out.keywordEligible = this._wordKeywordEligible;
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
this.classifyWord(out, ctx, tokenStart, errorStart);
|
|
2001
|
+
}
|
|
2002
|
+
isRedirectVariable(start, end) {
|
|
2003
|
+
const src = this.src;
|
|
2004
|
+
for (let i = start; i < end; i++) {
|
|
2005
|
+
const c = src.charCodeAt(i);
|
|
2006
|
+
if (c === CH_LBRACKET && i > start) {
|
|
2007
|
+
return (i + 2 < end && src.charCodeAt(end - 1) === CH_RBRACKET && this.findClosingBracket(i + 1, end) === end - 1);
|
|
2008
|
+
}
|
|
2009
|
+
if (c >= 128 || !(isIdChar[c] & (i === start ? 1 : 2)))
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2012
|
+
return end > start;
|
|
2013
|
+
}
|
|
2014
|
+
readArrayElement(out, tokenStart) {
|
|
2015
|
+
const close = this.findClosingBracket(tokenStart + 1);
|
|
2016
|
+
if (close === -1)
|
|
2017
|
+
return false;
|
|
2018
|
+
this.pos = close + 1;
|
|
2019
|
+
this.readWordText();
|
|
2020
|
+
setSpanToken(out, Token.Word, tokenStart, this.pos, true);
|
|
2021
|
+
return true;
|
|
2022
|
+
}
|
|
2023
|
+
classifyWord(out, ctx, tokenStart, errorStart) {
|
|
2024
|
+
const src = this.src;
|
|
2025
|
+
const raw = this._wordRaw;
|
|
2026
|
+
const hasExpansions = this._wordHasExpansions;
|
|
2027
|
+
const quoted = this._wordQuoted;
|
|
2028
|
+
const keywordEligible = this._wordKeywordEligible;
|
|
2029
|
+
let assignmentState = this._wordAssignmentState;
|
|
2030
|
+
const wordEnd = this.pos;
|
|
2031
|
+
const wordLen = wordEnd - tokenStart;
|
|
2032
|
+
if (ctx === LexContext.CasePattern) {
|
|
2033
|
+
let token = Token.Word;
|
|
2034
|
+
if (keywordEligible) {
|
|
2035
|
+
const reserved = raw
|
|
2036
|
+
? matchReservedWord(src, tokenStart, wordLen)
|
|
2037
|
+
: RESERVED_WORDS.get(this.wordValueOf(tokenStart, wordEnd));
|
|
2038
|
+
if (reserved === Token.Esac)
|
|
2039
|
+
token = reserved;
|
|
2040
|
+
}
|
|
2041
|
+
setSpanToken(out, token, tokenStart, wordEnd, raw);
|
|
2042
|
+
out.keywordEligible = keywordEligible;
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
// Only keyword classification needs a non-raw word's processed value.
|
|
2046
|
+
let value = null;
|
|
2047
|
+
if (!raw && !hasExpansions && keywordEligible && (ctx === LexContext.CommandStart || ctx === LexContext.TestMode)) {
|
|
2048
|
+
const nextCh = wordEnd < this.srcEnd ? src.charCodeAt(wordEnd) : 0;
|
|
2049
|
+
if ((!quoted && wordLen <= 16) || nextCh === CH_LT || nextCh === CH_GT) {
|
|
2050
|
+
value = this.wordValueOf(tokenStart, wordEnd);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
if (ctx === LexContext.CommandStart && keywordEligible) {
|
|
2054
|
+
if (raw) {
|
|
2055
|
+
if (wordLen <= 8) {
|
|
2056
|
+
const reserved = matchReservedWord(src, tokenStart, wordLen);
|
|
2057
|
+
if (reserved !== undefined) {
|
|
2058
|
+
setSpanToken(out, reserved, tokenStart, wordEnd, true);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (wordLen === 2 &&
|
|
2063
|
+
src.charCodeAt(tokenStart) === CH_LBRACKET &&
|
|
2064
|
+
src.charCodeAt(tokenStart + 1) === CH_LBRACKET) {
|
|
2065
|
+
setSpanToken(out, Token.DblLBracket, tokenStart, wordEnd, true);
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
else if (value !== null && value.length > 0) {
|
|
2070
|
+
const fc = value.charCodeAt(0);
|
|
2071
|
+
if ((fc >= CH_a && fc <= CH_z && value.length <= 8) || fc === CH_BANG || fc === CH_LBRACE || fc === CH_RBRACE) {
|
|
2072
|
+
const reserved = RESERVED_WORDS.get(value);
|
|
2073
|
+
if (reserved !== undefined) {
|
|
2074
|
+
setToken(out, reserved, value, tokenStart, wordEnd);
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (fc === CH_LBRACKET && value === "[[") {
|
|
2079
|
+
setToken(out, Token.DblLBracket, value, tokenStart, wordEnd);
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
if (ctx === LexContext.CommandStart || ctx === LexContext.CommandPrefix || ctx === LexContext.Declaration) {
|
|
2085
|
+
if (assignmentState === undefined) {
|
|
2086
|
+
// Fast-path word (value === raw span): detect assignment positionally.
|
|
2087
|
+
let eq = -1;
|
|
2088
|
+
let bracket = false;
|
|
2089
|
+
for (let i = tokenStart + 1; i < wordEnd; i++) {
|
|
2090
|
+
const c = src.charCodeAt(i);
|
|
2091
|
+
if (c === CH_EQ) {
|
|
2092
|
+
eq = i;
|
|
2093
|
+
break;
|
|
2094
|
+
}
|
|
2095
|
+
if (c === CH_LBRACKET)
|
|
2096
|
+
bracket = true;
|
|
2097
|
+
}
|
|
2098
|
+
if (eq !== -1 || (ctx !== LexContext.Declaration && bracket)) {
|
|
2099
|
+
assignmentState = scanAssignmentPrefix(src, tokenStart, wordEnd, ASSIGNMENT_NAME_START);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
if (assignmentState !== undefined) {
|
|
2103
|
+
if (ctx !== LexContext.Declaration && assignmentState >= ASSIGNMENT_INDEX_BASE) {
|
|
2104
|
+
if (wordEnd < this.srcEnd) {
|
|
2105
|
+
if (this._errors !== null)
|
|
2106
|
+
this._errors.length = errorStart;
|
|
2107
|
+
this.pos = tokenStart;
|
|
2108
|
+
this.readWordText(true, ctx, true);
|
|
2109
|
+
if (this.pos > wordEnd) {
|
|
2110
|
+
this.classifyWord(out, ctx, tokenStart, errorStart);
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
if (this._errors === null || this._errors.length === errorStart) {
|
|
2115
|
+
this.errors.push({ message: "unterminated array subscript", pos: src.indexOf("[", tokenStart) });
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
if (isMatchedAssignment(assignmentState)) {
|
|
2119
|
+
setSpanToken(out, Token.Assignment, tokenStart, wordEnd, raw);
|
|
2120
|
+
if (value !== null)
|
|
2121
|
+
out._value = value;
|
|
2122
|
+
out.assignmentOperatorPos = assignmentOperatorPos(assignmentState);
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
// `]]` is reserved only where it can close a `[[`; operands and wordlist entries, which
|
|
2128
|
+
// read Normal, are ordinary words, as is a `]]` demoted by a command prefix.
|
|
2129
|
+
if ((ctx === LexContext.CommandStart || ctx === LexContext.TestMode) && keywordEligible) {
|
|
2130
|
+
if (raw) {
|
|
2131
|
+
if (wordLen === 2 &&
|
|
2132
|
+
src.charCodeAt(tokenStart) === CH_RBRACKET &&
|
|
2133
|
+
src.charCodeAt(tokenStart + 1) === CH_RBRACKET) {
|
|
2134
|
+
setSpanToken(out, Token.DblRBracket, tokenStart, wordEnd, true);
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
else if (value === "]]") {
|
|
2139
|
+
setToken(out, Token.DblRBracket, value, tokenStart, wordEnd);
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
// FD number prefix: all-digit word followed by < or > → redirect with fd
|
|
2144
|
+
if (!hasExpansions && this.pos < this.srcEnd) {
|
|
2145
|
+
const nc = src.charCodeAt(this.pos);
|
|
2146
|
+
if (nc === CH_LT || nc === CH_GT) {
|
|
2147
|
+
if (raw) {
|
|
2148
|
+
const fc = src.charCodeAt(tokenStart);
|
|
2149
|
+
if (fc >= CH_0 && fc <= CH_9 && isAllDigitsRange(src, tokenStart, wordEnd)) {
|
|
2150
|
+
const fd = Number.parseInt(src.slice(tokenStart, wordEnd), 10);
|
|
2151
|
+
if (this.readRedirection(out, tokenStart)) {
|
|
2152
|
+
out.fileDescriptor = fd;
|
|
2153
|
+
out.descriptorEnd = wordEnd;
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (fc === CH_LBRACE &&
|
|
2158
|
+
src.charCodeAt(wordEnd - 1) === CH_RBRACE &&
|
|
2159
|
+
this.isRedirectVariable(tokenStart + 1, wordEnd - 1)) {
|
|
2160
|
+
const varname = src.slice(tokenStart + 1, wordEnd - 1);
|
|
2161
|
+
if (this.readRedirection(out, tokenStart)) {
|
|
2162
|
+
out.variableName = varname;
|
|
2163
|
+
out.descriptorEnd = wordEnd;
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
setSpanToken(out, Token.Word, tokenStart, wordEnd, raw);
|
|
2171
|
+
if (value !== null)
|
|
2172
|
+
out._value = value;
|
|
2173
|
+
out.keywordEligible = keywordEligible;
|
|
2174
|
+
}
|
|
2175
|
+
// `subscripts` re-reads a word known to stop inside an array subscript, where
|
|
2176
|
+
// metacharacters are ordinary text.
|
|
2177
|
+
// `scanning` marks the first scan of a token, which decides where the word ends.
|
|
2178
|
+
readWordText(subscripts = false, ctx = LexContext.Normal, scanning = false) {
|
|
2179
|
+
const src = this.src;
|
|
2180
|
+
const len = this.srcEnd;
|
|
2181
|
+
let pos = this.pos;
|
|
2182
|
+
// Fast path: scan a single run of plain chars (covers most words)
|
|
2183
|
+
const fastStart = pos;
|
|
2184
|
+
let exitCh = 0;
|
|
2185
|
+
while (pos < len) {
|
|
2186
|
+
const c = src.charCodeAt(pos);
|
|
2187
|
+
if (c < 128 && charType[c]) {
|
|
2188
|
+
exitCh = c;
|
|
2189
|
+
break;
|
|
2190
|
+
}
|
|
2191
|
+
pos++;
|
|
2192
|
+
}
|
|
2193
|
+
if (pos >= len ||
|
|
2194
|
+
(charType[exitCh] & 1 &&
|
|
2195
|
+
!(exitCh === CH_LPAREN && pos > fastStart && extglobPrefix[src.charCodeAt(pos - 1)]) &&
|
|
2196
|
+
!subscripts)) {
|
|
2197
|
+
this.pos = pos;
|
|
2198
|
+
this._wordText = (this._buildParts || this._buildValue) && pos > fastStart ? src.slice(fastStart, pos) : "";
|
|
2199
|
+
this._wordRaw = true;
|
|
2200
|
+
this._wordQuoted = false;
|
|
2201
|
+
this._wordHasExpansions = false;
|
|
2202
|
+
this._wordKeywordEligible = true;
|
|
2203
|
+
this._wordAssignmentState = undefined;
|
|
2204
|
+
if (this._buildParts) {
|
|
2205
|
+
this._wordParts =
|
|
2206
|
+
hasGlobCharacters(this._wordText) && ctx !== LexContext.ArrayElement
|
|
2207
|
+
? (parseGlobParts(src, fastStart, pos, undefined) ?? null)
|
|
2208
|
+
: null;
|
|
2209
|
+
}
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
// Slow path: word contains quotes, expansions, escapes, etc. `text` (the
|
|
2213
|
+
// processed value) is only assembled when building; the token path tracks
|
|
2214
|
+
// positions and flags plus the last value char (for extglob prefixes) and
|
|
2215
|
+
// whether the value still equals the raw span.
|
|
2216
|
+
const bp = this._buildParts;
|
|
2217
|
+
const bt = bp || this._buildValue;
|
|
2218
|
+
let text = bt && pos > fastStart ? src.slice(fastStart, pos) : "";
|
|
2219
|
+
let quoted = false;
|
|
2220
|
+
let hasExpansions = false;
|
|
2221
|
+
let keywordEligible = true;
|
|
2222
|
+
let valueIsRaw = true;
|
|
2223
|
+
let lastValueChar = pos > fastStart ? src.charCodeAt(pos - 1) : 0;
|
|
2224
|
+
let assignmentState = scanAssignmentPrefix(src, fastStart, pos, ASSIGNMENT_NAME_START);
|
|
2225
|
+
let parts;
|
|
2226
|
+
let litBuf = "";
|
|
2227
|
+
let litStart = 0;
|
|
2228
|
+
let hasGlobs = false;
|
|
2229
|
+
if (bp) {
|
|
2230
|
+
parts = [];
|
|
2231
|
+
litBuf = text; // fast-path prefix is literal
|
|
2232
|
+
litStart = fastStart;
|
|
2233
|
+
hasGlobs = text.length > 0 && hasGlobCharacters(text);
|
|
2234
|
+
}
|
|
2235
|
+
while (pos < len) {
|
|
2236
|
+
const ch = src.charCodeAt(pos);
|
|
2237
|
+
if (ch >= 128 || !charType[ch]) {
|
|
2238
|
+
const runStart = pos;
|
|
2239
|
+
pos++;
|
|
2240
|
+
while (pos < len) {
|
|
2241
|
+
const c = src.charCodeAt(pos);
|
|
2242
|
+
if (c < 128 && charType[c])
|
|
2243
|
+
break;
|
|
2244
|
+
pos++;
|
|
2245
|
+
}
|
|
2246
|
+
lastValueChar = src.charCodeAt(pos - 1);
|
|
2247
|
+
assignmentState = scanAssignmentPrefix(src, runStart, pos, assignmentState);
|
|
2248
|
+
if (bt) {
|
|
2249
|
+
const chunk = src.slice(runStart, pos);
|
|
2250
|
+
text += chunk;
|
|
2251
|
+
if (bp) {
|
|
2252
|
+
litBuf += chunk;
|
|
2253
|
+
hasGlobs = hasGlobs || hasGlobCharacters(chunk);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
if (charType[ch] & 1) {
|
|
2259
|
+
if (subscripts && assignmentState >= ASSIGNMENT_INDEX_BASE) {
|
|
2260
|
+
const close = this.findClosingBracket(pos);
|
|
2261
|
+
if (close !== -1) {
|
|
2262
|
+
const spanEnd = close + 1;
|
|
2263
|
+
// The scanner matched the innermost open bracket past quotes, escapes and nested
|
|
2264
|
+
// substitutions, so the jump closes exactly one subscript level.
|
|
2265
|
+
assignmentState =
|
|
2266
|
+
assignmentState === ASSIGNMENT_INDEX_BASE + 1 ? ASSIGNMENT_AFTER_INDEX : assignmentState - 1;
|
|
2267
|
+
lastValueChar = src.charCodeAt(close);
|
|
2268
|
+
if (bt) {
|
|
2269
|
+
const chunk = src.slice(pos, spanEnd);
|
|
2270
|
+
text += chunk;
|
|
2271
|
+
if (bp) {
|
|
2272
|
+
litBuf += chunk;
|
|
2273
|
+
hasGlobs = hasGlobs || hasGlobCharacters(chunk);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
pos = spanEnd;
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (ch === CH_LPAREN && lastValueChar < 128 && extglobPrefix[lastValueChar]) {
|
|
2281
|
+
if (ctx === LexContext.FunctionName ||
|
|
2282
|
+
(assignmentState >= ASSIGNMENT_INDEX_BASE &&
|
|
2283
|
+
(ctx === LexContext.CommandStart || ctx === LexContext.CommandPrefix)))
|
|
2284
|
+
break;
|
|
2285
|
+
// Default Bash command syntax takes precedence over the extglob extension.
|
|
2286
|
+
if (ctx === LexContext.CommandStart &&
|
|
2287
|
+
lastValueChar === CH_BANG &&
|
|
2288
|
+
keywordEligible &&
|
|
2289
|
+
src.charCodeAt(fastStart) === CH_BANG &&
|
|
2290
|
+
skipLineContinuations(src, fastStart + 1, len) === pos)
|
|
2291
|
+
break;
|
|
2292
|
+
const prefixChar = lastValueChar;
|
|
2293
|
+
// Bash opens an array value only right after the operator of a word that can assign;
|
|
2294
|
+
// elsewhere `(` ends the word. Rescans of a known word span keep its value.
|
|
2295
|
+
if (prefixChar === CH_EQ &&
|
|
2296
|
+
scanning &&
|
|
2297
|
+
!((ctx === LexContext.CommandStart ||
|
|
2298
|
+
ctx === LexContext.CommandPrefix ||
|
|
2299
|
+
ctx === LexContext.Declaration ||
|
|
2300
|
+
ctx === LexContext.EvalArgument) &&
|
|
2301
|
+
isMatchedAssignment(assignmentState) &&
|
|
2302
|
+
skipLineContinuations(src, assignmentOperatorPos(assignmentState) + 1, len) === pos))
|
|
2303
|
+
break;
|
|
2304
|
+
const innerStart = pos + 1;
|
|
2305
|
+
const close = this.findClosingShellDelimiter(innerStart, len, CH_RPAREN, prefixChar === CH_EQ);
|
|
2306
|
+
if (close === -1 && !scanning && !subscripts && assignmentState >= ASSIGNMENT_INDEX_BASE)
|
|
2307
|
+
break;
|
|
2308
|
+
if (ctx === LexContext.CommandStart &&
|
|
2309
|
+
prefixChar !== CH_EQ &&
|
|
2310
|
+
close !== -1 &&
|
|
2311
|
+
skipBlanks(src, innerStart, close) === close)
|
|
2312
|
+
break;
|
|
2313
|
+
keywordEligible = false;
|
|
2314
|
+
const patternEnd = close === -1 ? len : close;
|
|
2315
|
+
pos = close === -1 ? len : close + 1;
|
|
2316
|
+
if (close === -1)
|
|
2317
|
+
this.errors.push({ message: "unterminated extended glob", pos: innerStart - 2 });
|
|
2318
|
+
lastValueChar = src.charCodeAt(pos - 1);
|
|
2319
|
+
if (bt) {
|
|
2320
|
+
const eg = "(" + src.slice(innerStart, pos);
|
|
2321
|
+
text += eg;
|
|
2322
|
+
// Create ExtendedGlob part for real extglob operators (not = which is array assignment)
|
|
2323
|
+
if (bp && prefixChar !== CH_EQ) {
|
|
2324
|
+
let prefixPos = innerStart - 2;
|
|
2325
|
+
while (src.charCodeAt(prefixPos) === CH_NL && src.charCodeAt(prefixPos - 1) === CH_BACKSLASH) {
|
|
2326
|
+
prefixPos -= 2;
|
|
2327
|
+
}
|
|
2328
|
+
// Remove the prefix char from litBuf (it was appended in the previous iteration)
|
|
2329
|
+
if (litBuf.length > 0) {
|
|
2330
|
+
const trimmed = litBuf.slice(0, -1);
|
|
2331
|
+
if (prefixPos > litStart)
|
|
2332
|
+
parts.push({
|
|
2333
|
+
type: "Literal",
|
|
2334
|
+
pos: litStart,
|
|
2335
|
+
end: prefixPos,
|
|
2336
|
+
value: trimmed,
|
|
2337
|
+
text: src.slice(litStart, prefixPos),
|
|
2338
|
+
});
|
|
2339
|
+
litBuf = "";
|
|
2340
|
+
}
|
|
2341
|
+
const op = extglobOp[prefixChar];
|
|
2342
|
+
parts.push({
|
|
2343
|
+
type: "ExtendedGlob",
|
|
2344
|
+
pos: prefixPos,
|
|
2345
|
+
end: pos,
|
|
2346
|
+
text: src.slice(prefixPos, pos),
|
|
2347
|
+
operator: op,
|
|
2348
|
+
pattern: src.slice(innerStart, patternEnd),
|
|
2349
|
+
parts: this.subFieldParts(innerStart, patternEnd),
|
|
2350
|
+
});
|
|
2351
|
+
litStart = pos;
|
|
2352
|
+
}
|
|
2353
|
+
else if (bp) {
|
|
2354
|
+
litBuf += eg;
|
|
2355
|
+
hasGlobs = hasGlobs || hasGlobCharacters(eg);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
break;
|
|
2361
|
+
}
|
|
2362
|
+
if (ch === CH_BACKSLASH) {
|
|
2363
|
+
pos++;
|
|
2364
|
+
if (pos < len) {
|
|
2365
|
+
if (src.charCodeAt(pos) === CH_NL) {
|
|
2366
|
+
pos++;
|
|
2367
|
+
valueIsRaw = false;
|
|
2368
|
+
}
|
|
2369
|
+
else {
|
|
2370
|
+
if (assignmentState >= 0 && assignmentState < ASSIGNMENT_INDEX_BASE)
|
|
2371
|
+
assignmentState = ASSIGNMENT_INVALID;
|
|
2372
|
+
quoted = true;
|
|
2373
|
+
keywordEligible = false;
|
|
2374
|
+
valueIsRaw = false;
|
|
2375
|
+
lastValueChar = src.charCodeAt(pos);
|
|
2376
|
+
if (bt) {
|
|
2377
|
+
text += src[pos];
|
|
2378
|
+
if (bp)
|
|
2379
|
+
litBuf += src[pos];
|
|
2380
|
+
}
|
|
2381
|
+
pos++;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
else {
|
|
2385
|
+
// A backslash at end of input escapes nothing and stays as itself.
|
|
2386
|
+
if (assignmentState >= 0 && assignmentState < ASSIGNMENT_INDEX_BASE)
|
|
2387
|
+
assignmentState = ASSIGNMENT_INVALID;
|
|
2388
|
+
quoted = true;
|
|
2389
|
+
keywordEligible = false;
|
|
2390
|
+
lastValueChar = CH_BACKSLASH;
|
|
2391
|
+
if (bt) {
|
|
2392
|
+
text += "\\";
|
|
2393
|
+
if (bp)
|
|
2394
|
+
litBuf += "\\";
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
if (ch === CH_SQUOTE) {
|
|
2400
|
+
const sqStart = pos;
|
|
2401
|
+
if (assignmentState >= 0 && assignmentState < ASSIGNMENT_INDEX_BASE)
|
|
2402
|
+
assignmentState = ASSIGNMENT_INVALID;
|
|
2403
|
+
quoted = true;
|
|
2404
|
+
keywordEligible = false;
|
|
2405
|
+
valueIsRaw = false;
|
|
2406
|
+
pos++;
|
|
2407
|
+
const start = pos;
|
|
2408
|
+
while (pos < len && src.charCodeAt(pos) !== CH_SQUOTE)
|
|
2409
|
+
pos++;
|
|
2410
|
+
if (pos > start)
|
|
2411
|
+
lastValueChar = src.charCodeAt(pos - 1);
|
|
2412
|
+
const value = bt ? src.slice(start, pos) : "";
|
|
2413
|
+
if (bt)
|
|
2414
|
+
text += value;
|
|
2415
|
+
if (pos < len)
|
|
2416
|
+
pos++;
|
|
2417
|
+
else
|
|
2418
|
+
this.errors.push({ message: "unterminated single quote", pos: start - 1 });
|
|
2419
|
+
if (bp) {
|
|
2420
|
+
if (sqStart > litStart) {
|
|
2421
|
+
parts.push({
|
|
2422
|
+
type: "Literal",
|
|
2423
|
+
pos: litStart,
|
|
2424
|
+
end: sqStart,
|
|
2425
|
+
value: litBuf,
|
|
2426
|
+
text: src.slice(litStart, sqStart),
|
|
2427
|
+
});
|
|
2428
|
+
litBuf = "";
|
|
2429
|
+
}
|
|
2430
|
+
parts.push({ type: "SingleQuoted", pos: sqStart, end: pos, value, text: src.slice(sqStart, pos) });
|
|
2431
|
+
litStart = pos;
|
|
2432
|
+
}
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
if (ch === CH_DQUOTE) {
|
|
2436
|
+
const dqStart = pos;
|
|
2437
|
+
if (assignmentState >= 0 && assignmentState < ASSIGNMENT_INDEX_BASE)
|
|
2438
|
+
assignmentState = ASSIGNMENT_INVALID;
|
|
2439
|
+
quoted = true;
|
|
2440
|
+
keywordEligible = false;
|
|
2441
|
+
valueIsRaw = false;
|
|
2442
|
+
pos++;
|
|
2443
|
+
this.pos = pos;
|
|
2444
|
+
this.readDoubleQuoted();
|
|
2445
|
+
pos = this.pos;
|
|
2446
|
+
if (this._dqEnd > dqStart + 1)
|
|
2447
|
+
lastValueChar = src.charCodeAt(this._dqEnd - 1);
|
|
2448
|
+
if (this._dqHasExpansions)
|
|
2449
|
+
hasExpansions = true;
|
|
2450
|
+
if (bt)
|
|
2451
|
+
text += this._dqText;
|
|
2452
|
+
if (bp) {
|
|
2453
|
+
if (dqStart > litStart) {
|
|
2454
|
+
parts.push({
|
|
2455
|
+
type: "Literal",
|
|
2456
|
+
pos: litStart,
|
|
2457
|
+
end: dqStart,
|
|
2458
|
+
value: litBuf,
|
|
2459
|
+
text: src.slice(litStart, dqStart),
|
|
2460
|
+
});
|
|
2461
|
+
litBuf = "";
|
|
2462
|
+
}
|
|
2463
|
+
const dqText = src.slice(dqStart, pos);
|
|
2464
|
+
parts.push({
|
|
2465
|
+
type: "DoubleQuoted",
|
|
2466
|
+
pos: dqStart,
|
|
2467
|
+
end: pos,
|
|
2468
|
+
text: dqText,
|
|
2469
|
+
parts: this._dqParts ?? [
|
|
2470
|
+
{
|
|
2471
|
+
type: "Literal",
|
|
2472
|
+
pos: dqStart + 1,
|
|
2473
|
+
end: this._dqEnd,
|
|
2474
|
+
value: this._dqText,
|
|
2475
|
+
text: src.slice(dqStart + 1, this._dqEnd),
|
|
2476
|
+
},
|
|
2477
|
+
],
|
|
2478
|
+
});
|
|
2479
|
+
litStart = pos;
|
|
2480
|
+
}
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
if (ch === CH_DOLLAR) {
|
|
2484
|
+
keywordEligible = false;
|
|
2485
|
+
const dollarStart = pos;
|
|
2486
|
+
if (assignmentState >= 0 && assignmentState < ASSIGNMENT_INDEX_BASE)
|
|
2487
|
+
assignmentState = ASSIGNMENT_INVALID;
|
|
2488
|
+
this.pos = pos;
|
|
2489
|
+
this.readDollar();
|
|
2490
|
+
pos = this.pos;
|
|
2491
|
+
if (!this._resultIsRaw)
|
|
2492
|
+
valueIsRaw = false;
|
|
2493
|
+
if (pos > dollarStart)
|
|
2494
|
+
lastValueChar = src.charCodeAt(pos - 1);
|
|
2495
|
+
if (this._resultHasExpansion)
|
|
2496
|
+
hasExpansions = true;
|
|
2497
|
+
if (bt)
|
|
2498
|
+
text += this._resultText;
|
|
2499
|
+
if (bp) {
|
|
2500
|
+
if (this._resultPart) {
|
|
2501
|
+
if (dollarStart > litStart) {
|
|
2502
|
+
parts.push({
|
|
2503
|
+
type: "Literal",
|
|
2504
|
+
pos: litStart,
|
|
2505
|
+
end: dollarStart,
|
|
2506
|
+
value: litBuf,
|
|
2507
|
+
text: src.slice(litStart, dollarStart),
|
|
2508
|
+
});
|
|
2509
|
+
litBuf = "";
|
|
2510
|
+
}
|
|
2511
|
+
parts.push(this._resultPart);
|
|
2512
|
+
litStart = pos;
|
|
2513
|
+
}
|
|
2514
|
+
else {
|
|
2515
|
+
litBuf += this._resultText;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2520
|
+
if (ch === CH_BACKTICK) {
|
|
2521
|
+
keywordEligible = false;
|
|
2522
|
+
const btStart = pos;
|
|
2523
|
+
if (assignmentState >= 0 && assignmentState < ASSIGNMENT_INDEX_BASE)
|
|
2524
|
+
assignmentState = ASSIGNMENT_INVALID;
|
|
2525
|
+
this.pos = pos;
|
|
2526
|
+
this.readBacktickExpansion();
|
|
2527
|
+
pos = this.pos;
|
|
2528
|
+
valueIsRaw = false;
|
|
2529
|
+
lastValueChar = src.charCodeAt(pos - 1);
|
|
2530
|
+
hasExpansions = true;
|
|
2531
|
+
if (bt)
|
|
2532
|
+
text += this._resultText;
|
|
2533
|
+
if (bp) {
|
|
2534
|
+
if (btStart > litStart) {
|
|
2535
|
+
parts.push({
|
|
2536
|
+
type: "Literal",
|
|
2537
|
+
pos: litStart,
|
|
2538
|
+
end: btStart,
|
|
2539
|
+
value: litBuf,
|
|
2540
|
+
text: src.slice(litStart, btStart),
|
|
2541
|
+
});
|
|
2542
|
+
litBuf = "";
|
|
2543
|
+
}
|
|
2544
|
+
parts.push(this._resultPart);
|
|
2545
|
+
litStart = pos;
|
|
2546
|
+
}
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
if (ch === CH_LBRACE) {
|
|
2550
|
+
if (assignmentState >= 0 && assignmentState < ASSIGNMENT_INDEX_BASE)
|
|
2551
|
+
assignmentState = ASSIGNMENT_INVALID;
|
|
2552
|
+
const braceEnd = this.scanBraceExpansion(pos);
|
|
2553
|
+
if (braceEnd > 0) {
|
|
2554
|
+
keywordEligible = false;
|
|
2555
|
+
lastValueChar = src.charCodeAt(braceEnd - 1);
|
|
2556
|
+
if (bt) {
|
|
2557
|
+
const braceText = src.slice(pos, braceEnd);
|
|
2558
|
+
text += braceText;
|
|
2559
|
+
if (bp) {
|
|
2560
|
+
if (pos > litStart) {
|
|
2561
|
+
parts.push({
|
|
2562
|
+
type: "Literal",
|
|
2563
|
+
pos: litStart,
|
|
2564
|
+
end: pos,
|
|
2565
|
+
value: litBuf,
|
|
2566
|
+
text: src.slice(litStart, pos),
|
|
2567
|
+
});
|
|
2568
|
+
litBuf = "";
|
|
2569
|
+
}
|
|
2570
|
+
parts.push({
|
|
2571
|
+
type: "BraceExpansion",
|
|
2572
|
+
pos,
|
|
2573
|
+
end: braceEnd,
|
|
2574
|
+
text: braceText,
|
|
2575
|
+
parts: this.subFieldParts(pos + 1, braceEnd - 1),
|
|
2576
|
+
});
|
|
2577
|
+
litStart = braceEnd;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
pos = braceEnd;
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
lastValueChar = CH_LBRACE;
|
|
2584
|
+
if (bt) {
|
|
2585
|
+
text += "{";
|
|
2586
|
+
if (bp)
|
|
2587
|
+
litBuf += "{";
|
|
2588
|
+
}
|
|
2589
|
+
pos++;
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
pos++;
|
|
2593
|
+
}
|
|
2594
|
+
if (bp && pos > litStart)
|
|
2595
|
+
parts.push({ type: "Literal", pos: litStart, end: pos, value: litBuf, text: src.slice(litStart, pos) });
|
|
2596
|
+
this.pos = pos;
|
|
2597
|
+
this._wordText = text;
|
|
2598
|
+
this._wordRaw = valueIsRaw;
|
|
2599
|
+
this._wordQuoted = quoted;
|
|
2600
|
+
this._wordHasExpansions = hasExpansions;
|
|
2601
|
+
this._wordKeywordEligible = keywordEligible;
|
|
2602
|
+
this._wordAssignmentState = assignmentState;
|
|
2603
|
+
if (bp) {
|
|
2604
|
+
// Only store parts if they add structure beyond a single literal
|
|
2605
|
+
this._wordParts = parts.length > 1 || (parts.length === 1 && parts[0].type !== "Literal") ? parts : null;
|
|
2606
|
+
if (hasGlobs && ctx !== LexContext.ArrayElement)
|
|
2607
|
+
this._wordParts = parseGlobParts(src, fastStart, pos, this._wordParts) ?? null;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
// Arithmetic text gets expansions and quote removal but no process substitution.
|
|
2611
|
+
readInnerWordText(insideDoubleQuotes = false, arithmetic = false, globs = false) {
|
|
2612
|
+
const src = this.src;
|
|
2613
|
+
const len = this.srcEnd;
|
|
2614
|
+
let pos = this.pos;
|
|
2615
|
+
const start = pos;
|
|
2616
|
+
let text = "";
|
|
2617
|
+
const bp = this._buildParts;
|
|
2618
|
+
let parts;
|
|
2619
|
+
let litBuf = "";
|
|
2620
|
+
let litStart = 0;
|
|
2621
|
+
let preserveLiteral = false;
|
|
2622
|
+
let hasGlobs = false;
|
|
2623
|
+
if (bp) {
|
|
2624
|
+
parts = [];
|
|
2625
|
+
litStart = pos;
|
|
2626
|
+
}
|
|
2627
|
+
while (pos < len) {
|
|
2628
|
+
const ch = src.charCodeAt(pos);
|
|
2629
|
+
if (ch === CH_BACKSLASH) {
|
|
2630
|
+
pos++;
|
|
2631
|
+
if (pos < len) {
|
|
2632
|
+
if (src.charCodeAt(pos) === CH_NL) {
|
|
2633
|
+
pos++;
|
|
2634
|
+
}
|
|
2635
|
+
else {
|
|
2636
|
+
const next = src.charCodeAt(pos);
|
|
2637
|
+
if (insideDoubleQuotes &&
|
|
2638
|
+
next !== CH_DOLLAR &&
|
|
2639
|
+
next !== CH_BACKTICK &&
|
|
2640
|
+
next !== CH_DQUOTE &&
|
|
2641
|
+
next !== CH_BACKSLASH &&
|
|
2642
|
+
next !== CH_RBRACE) {
|
|
2643
|
+
text += "\\";
|
|
2644
|
+
if (bp)
|
|
2645
|
+
litBuf += "\\";
|
|
2646
|
+
preserveLiteral = true;
|
|
2647
|
+
}
|
|
2648
|
+
const escaped = src[pos++];
|
|
2649
|
+
text += escaped;
|
|
2650
|
+
if (bp)
|
|
2651
|
+
litBuf += escaped;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
if (ch === CH_SQUOTE && !insideDoubleQuotes) {
|
|
2657
|
+
const sqStart = pos;
|
|
2658
|
+
pos++;
|
|
2659
|
+
const start = pos;
|
|
2660
|
+
while (pos < len && src.charCodeAt(pos) !== CH_SQUOTE)
|
|
2661
|
+
pos++;
|
|
2662
|
+
const value = src.slice(start, pos);
|
|
2663
|
+
text += value;
|
|
2664
|
+
if (pos < len)
|
|
2665
|
+
pos++;
|
|
2666
|
+
if (bp) {
|
|
2667
|
+
if (sqStart > litStart) {
|
|
2668
|
+
parts.push({
|
|
2669
|
+
type: "Literal",
|
|
2670
|
+
pos: litStart,
|
|
2671
|
+
end: sqStart,
|
|
2672
|
+
value: litBuf,
|
|
2673
|
+
text: src.slice(litStart, sqStart),
|
|
2674
|
+
});
|
|
2675
|
+
litBuf = "";
|
|
2676
|
+
}
|
|
2677
|
+
parts.push({ type: "SingleQuoted", pos: sqStart, end: pos, value, text: src.slice(sqStart, pos) });
|
|
2678
|
+
litStart = pos;
|
|
2679
|
+
}
|
|
2680
|
+
continue;
|
|
2681
|
+
}
|
|
2682
|
+
if (ch === CH_DQUOTE) {
|
|
2683
|
+
const dqStart = pos;
|
|
2684
|
+
pos++;
|
|
2685
|
+
this.pos = pos;
|
|
2686
|
+
this.readDoubleQuoted(insideDoubleQuotes);
|
|
2687
|
+
pos = this.pos;
|
|
2688
|
+
text += this._dqText;
|
|
2689
|
+
if (bp) {
|
|
2690
|
+
if (dqStart > litStart) {
|
|
2691
|
+
parts.push({
|
|
2692
|
+
type: "Literal",
|
|
2693
|
+
pos: litStart,
|
|
2694
|
+
end: dqStart,
|
|
2695
|
+
value: litBuf,
|
|
2696
|
+
text: src.slice(litStart, dqStart),
|
|
2697
|
+
});
|
|
2698
|
+
litBuf = "";
|
|
2699
|
+
}
|
|
2700
|
+
const dqText = src.slice(dqStart, pos);
|
|
2701
|
+
parts.push({
|
|
2702
|
+
type: "DoubleQuoted",
|
|
2703
|
+
pos: dqStart,
|
|
2704
|
+
end: pos,
|
|
2705
|
+
text: dqText,
|
|
2706
|
+
parts: this._dqParts ?? [
|
|
2707
|
+
{
|
|
2708
|
+
type: "Literal",
|
|
2709
|
+
pos: dqStart + 1,
|
|
2710
|
+
end: this._dqEnd,
|
|
2711
|
+
value: this._dqText,
|
|
2712
|
+
text: src.slice(dqStart + 1, this._dqEnd),
|
|
2713
|
+
},
|
|
2714
|
+
],
|
|
2715
|
+
});
|
|
2716
|
+
litStart = pos;
|
|
2717
|
+
}
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
if (ch === CH_DOLLAR) {
|
|
2721
|
+
const dollarStart = pos;
|
|
2722
|
+
this.pos = pos;
|
|
2723
|
+
this.readDollar(insideDoubleQuotes);
|
|
2724
|
+
pos = this.pos;
|
|
2725
|
+
text += this._resultText;
|
|
2726
|
+
if (bp) {
|
|
2727
|
+
if (this._resultPart) {
|
|
2728
|
+
if (dollarStart > litStart) {
|
|
2729
|
+
parts.push({
|
|
2730
|
+
type: "Literal",
|
|
2731
|
+
pos: litStart,
|
|
2732
|
+
end: dollarStart,
|
|
2733
|
+
value: litBuf,
|
|
2734
|
+
text: src.slice(litStart, dollarStart),
|
|
2735
|
+
});
|
|
2736
|
+
litBuf = "";
|
|
2737
|
+
}
|
|
2738
|
+
parts.push(this._resultPart);
|
|
2739
|
+
litStart = pos;
|
|
2740
|
+
}
|
|
2741
|
+
else {
|
|
2742
|
+
litBuf += this._resultText;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
if (ch === CH_BACKTICK) {
|
|
2748
|
+
const btStart = pos;
|
|
2749
|
+
this.pos = pos;
|
|
2750
|
+
this.readBacktickExpansion(insideDoubleQuotes);
|
|
2751
|
+
pos = this.pos;
|
|
2752
|
+
text += this._resultText;
|
|
2753
|
+
if (bp) {
|
|
2754
|
+
if (btStart > litStart) {
|
|
2755
|
+
parts.push({
|
|
2756
|
+
type: "Literal",
|
|
2757
|
+
pos: litStart,
|
|
2758
|
+
end: btStart,
|
|
2759
|
+
value: litBuf,
|
|
2760
|
+
text: src.slice(litStart, btStart),
|
|
2761
|
+
});
|
|
2762
|
+
litBuf = "";
|
|
2763
|
+
}
|
|
2764
|
+
parts.push(this._resultPart);
|
|
2765
|
+
litStart = pos;
|
|
2766
|
+
}
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
if (!insideDoubleQuotes &&
|
|
2770
|
+
!arithmetic &&
|
|
2771
|
+
(ch === CH_LT || ch === CH_GT) &&
|
|
2772
|
+
pos + 1 < len &&
|
|
2773
|
+
src.charCodeAt(pos + 1) === CH_LPAREN) {
|
|
2774
|
+
const psStart = pos;
|
|
2775
|
+
this.pos = pos + 2;
|
|
2776
|
+
const inner = this.extractBalanced();
|
|
2777
|
+
pos = this.pos;
|
|
2778
|
+
const raw = src.slice(psStart, pos);
|
|
2779
|
+
text += raw;
|
|
2780
|
+
if (bp) {
|
|
2781
|
+
if (psStart > litStart) {
|
|
2782
|
+
parts.push({
|
|
2783
|
+
type: "Literal",
|
|
2784
|
+
pos: litStart,
|
|
2785
|
+
end: psStart,
|
|
2786
|
+
value: litBuf,
|
|
2787
|
+
text: src.slice(litStart, psStart),
|
|
2788
|
+
});
|
|
2789
|
+
litBuf = "";
|
|
2790
|
+
}
|
|
2791
|
+
parts.push(this.processSubstitution(psStart, raw, ch === CH_LT ? "<" : ">", inner));
|
|
2792
|
+
litStart = pos;
|
|
2793
|
+
}
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
text += src[pos];
|
|
2797
|
+
if (bp) {
|
|
2798
|
+
litBuf += src[pos];
|
|
2799
|
+
if (globs && (ch === CH_STAR || ch === CH_QUESTION || ch === CH_LBRACKET))
|
|
2800
|
+
hasGlobs = true;
|
|
2801
|
+
}
|
|
2802
|
+
pos++;
|
|
2803
|
+
}
|
|
2804
|
+
if (bp && pos > litStart)
|
|
2805
|
+
parts.push({ type: "Literal", pos: litStart, end: pos, value: litBuf, text: src.slice(litStart, pos) });
|
|
2806
|
+
this.pos = pos;
|
|
2807
|
+
this._wordText = text;
|
|
2808
|
+
this._wordRaw = false;
|
|
2809
|
+
this._wordQuoted = false;
|
|
2810
|
+
this._wordHasExpansions = false;
|
|
2811
|
+
this._wordKeywordEligible = false;
|
|
2812
|
+
if (bp) {
|
|
2813
|
+
this._wordParts =
|
|
2814
|
+
parts.length > 1 || (parts.length === 1 && (parts[0].type !== "Literal" || preserveLiteral)) ? parts : null;
|
|
2815
|
+
if (hasGlobs)
|
|
2816
|
+
this._wordParts = parseGlobParts(src, start, pos, this._wordParts) ?? null;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
subFieldParts(start, end) {
|
|
2820
|
+
if (this._nestingDepth >= MAX_SYNTAX_NESTING)
|
|
2821
|
+
return undefined;
|
|
2822
|
+
return hasEmbeddedWordStructure(this.src, start, end)
|
|
2823
|
+
? this.parseSubFieldWord(start, end).parts
|
|
2824
|
+
: parseGlobParts(this.src, start, end, undefined);
|
|
2825
|
+
}
|
|
2826
|
+
// Parse a parameter-expansion sub-field (operand, slice bound, replacement pattern) over
|
|
2827
|
+
// the window [start, end) of the original source. Parsing in place — rather than against a
|
|
2828
|
+
// detached slice — gives the word and any nested substitutions absolute offsets, and
|
|
2829
|
+
// composes through nested ${...}. The `${...}` inner is a verbatim substring of the
|
|
2830
|
+
// source, so every sub-field offset maps straight back.
|
|
2831
|
+
parseSubFieldWord(start, end, context = "word", arithmetic = false) {
|
|
2832
|
+
if (start >= end)
|
|
2833
|
+
return new WordImpl("", start, start);
|
|
2834
|
+
// Past the shared budget the sub-field keeps its raw text without parts.
|
|
2835
|
+
if (this._nestingDepth >= MAX_SYNTAX_NESTING)
|
|
2836
|
+
return new WordImpl(this.src.slice(start, end), start, end);
|
|
2837
|
+
this._nestingDepth++;
|
|
2838
|
+
const savedEnd = this.srcEnd;
|
|
2839
|
+
const savedPos = this.pos;
|
|
2840
|
+
const savedText = this._wordText;
|
|
2841
|
+
const savedParts = this._wordParts;
|
|
2842
|
+
const savedQuoted = this._wordQuoted;
|
|
2843
|
+
const savedKeywordEligible = this._wordKeywordEligible;
|
|
2844
|
+
this.srcEnd = end;
|
|
2845
|
+
this.pos = start;
|
|
2846
|
+
this.readInnerWordText(context === "quoted", arithmetic, context === "word");
|
|
2847
|
+
// text is the raw source span (so source.slice(pos, end) === text holds, as for every
|
|
2848
|
+
// other word); value resolves quotes via parts.
|
|
2849
|
+
const word = new WordImpl(this.src.slice(start, end), start, end);
|
|
2850
|
+
if (this._buildParts) {
|
|
2851
|
+
word.parts = this._wordParts ?? undefined;
|
|
2852
|
+
}
|
|
2853
|
+
this.srcEnd = savedEnd;
|
|
2854
|
+
this.pos = savedPos;
|
|
2855
|
+
this._wordText = savedText;
|
|
2856
|
+
this._wordParts = savedParts;
|
|
2857
|
+
this._wordQuoted = savedQuoted;
|
|
2858
|
+
this._wordKeywordEligible = savedKeywordEligible;
|
|
2859
|
+
this._nestingDepth--;
|
|
2860
|
+
return word;
|
|
2861
|
+
}
|
|
2862
|
+
skipSQ() {
|
|
2863
|
+
while (this.pos < this.srcEnd && this.src.charCodeAt(this.pos) !== CH_SQUOTE)
|
|
2864
|
+
this.pos++;
|
|
2865
|
+
if (this.pos < this.srcEnd)
|
|
2866
|
+
this.pos++;
|
|
2867
|
+
}
|
|
2868
|
+
skipAnsiCQuoted() {
|
|
2869
|
+
const quotePos = this.pos - 1;
|
|
2870
|
+
const result = decodeAnsiCQuoted(this.src, this.pos, this.srcEnd);
|
|
2871
|
+
this.pos = result.end;
|
|
2872
|
+
if (!result.closed)
|
|
2873
|
+
this.errors.push({ message: "unterminated ANSI-C quote", pos: quotePos });
|
|
2874
|
+
}
|
|
2875
|
+
// Plain quoted text is skipped here; the first `$(`, `${`, `$$`, backtick or the end of input
|
|
2876
|
+
// hands the rest to the quote scanner, which owns substitution and nesting-limit decisions.
|
|
2877
|
+
skipDQ() {
|
|
2878
|
+
const src = this.src;
|
|
2879
|
+
const len = this.srcEnd;
|
|
2880
|
+
this._unbalanced = false;
|
|
2881
|
+
if (this._nestingDepth < MAX_SYNTAX_NESTING) {
|
|
2882
|
+
while (this.pos < len) {
|
|
2883
|
+
const ch = src.charCodeAt(this.pos);
|
|
2884
|
+
if (ch === CH_DQUOTE) {
|
|
2885
|
+
this.pos++;
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (ch === CH_BACKSLASH) {
|
|
2889
|
+
this.pos = Math.min(this.pos + 2, len);
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
if (ch === CH_BACKTICK)
|
|
2893
|
+
break;
|
|
2894
|
+
if (ch === CH_DOLLAR) {
|
|
2895
|
+
const next = src.charCodeAt(this.pos + 1);
|
|
2896
|
+
if (next === CH_LPAREN || next === CH_LBRACE || next === CH_DOLLAR)
|
|
2897
|
+
break;
|
|
2898
|
+
}
|
|
2899
|
+
this.pos++;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
this.extractBalanced(true, true);
|
|
2903
|
+
}
|
|
2904
|
+
skipSpacesAndTabs() {
|
|
2905
|
+
this.pos = skipBlanks(this.src, this.pos, this.srcEnd);
|
|
2906
|
+
}
|
|
2907
|
+
readDoubleQuoted(unescapeAll = false) {
|
|
2908
|
+
const src = this.src;
|
|
2909
|
+
const len = this.srcEnd;
|
|
2910
|
+
const contentStart = this.pos;
|
|
2911
|
+
let hasExpansions = false;
|
|
2912
|
+
const bp = this._buildParts;
|
|
2913
|
+
const bt = bp || this._buildValue;
|
|
2914
|
+
// Fast path: pure literal content (no $, `, or \ — just find closing ")
|
|
2915
|
+
if (!bp) {
|
|
2916
|
+
let p = this.pos;
|
|
2917
|
+
while (p < len) {
|
|
2918
|
+
const c = src.charCodeAt(p);
|
|
2919
|
+
if (c === CH_DQUOTE) {
|
|
2920
|
+
this._dqText = bt ? src.slice(contentStart, p) : "";
|
|
2921
|
+
this._dqEnd = p;
|
|
2922
|
+
this.pos = p + 1;
|
|
2923
|
+
this._dqHasExpansions = false;
|
|
2924
|
+
this._dqParts = null;
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
if (c === CH_DOLLAR || c === CH_BACKTICK || c === CH_BACKSLASH)
|
|
2928
|
+
break;
|
|
2929
|
+
p++;
|
|
2930
|
+
}
|
|
2931
|
+
// Fall through to general path
|
|
2932
|
+
}
|
|
2933
|
+
let text = "";
|
|
2934
|
+
let parts = null;
|
|
2935
|
+
let litBuf = "";
|
|
2936
|
+
let litStart = bp ? this.pos : 0;
|
|
2937
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_DQUOTE) {
|
|
2938
|
+
// Scan run of plain chars inside double quotes
|
|
2939
|
+
const runStart = this.pos;
|
|
2940
|
+
while (this.pos < len) {
|
|
2941
|
+
const c = src.charCodeAt(this.pos);
|
|
2942
|
+
if (c === CH_DQUOTE || c === CH_BACKSLASH || c === CH_DOLLAR || c === CH_BACKTICK)
|
|
2943
|
+
break;
|
|
2944
|
+
this.pos++;
|
|
2945
|
+
}
|
|
2946
|
+
if (bt && this.pos > runStart) {
|
|
2947
|
+
const chunk = src.slice(runStart, this.pos);
|
|
2948
|
+
text += chunk;
|
|
2949
|
+
if (bp)
|
|
2950
|
+
litBuf += chunk;
|
|
2951
|
+
}
|
|
2952
|
+
if (this.pos >= len || src.charCodeAt(this.pos) === CH_DQUOTE)
|
|
2953
|
+
break;
|
|
2954
|
+
const ch = src.charCodeAt(this.pos);
|
|
2955
|
+
if (ch === CH_BACKSLASH) {
|
|
2956
|
+
this.pos++;
|
|
2957
|
+
if (this.pos < len) {
|
|
2958
|
+
const next = src.charCodeAt(this.pos);
|
|
2959
|
+
if (next === CH_NL) {
|
|
2960
|
+
this.pos++;
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
if (bt) {
|
|
2964
|
+
if (unescapeAll ||
|
|
2965
|
+
next === CH_DOLLAR ||
|
|
2966
|
+
next === CH_BACKTICK ||
|
|
2967
|
+
next === CH_DQUOTE ||
|
|
2968
|
+
next === CH_BACKSLASH) {
|
|
2969
|
+
const c = src[this.pos];
|
|
2970
|
+
text += c;
|
|
2971
|
+
if (bp)
|
|
2972
|
+
litBuf += c;
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
const pair = "\\" + src[this.pos];
|
|
2976
|
+
text += pair;
|
|
2977
|
+
if (bp)
|
|
2978
|
+
litBuf += pair;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
this.pos++;
|
|
2982
|
+
}
|
|
2983
|
+
continue;
|
|
2984
|
+
}
|
|
2985
|
+
if (ch === CH_DOLLAR) {
|
|
2986
|
+
// Inside double quotes `$"` and `$'` are a literal $: no locale string, no ANSI-C quote.
|
|
2987
|
+
const afterDollar = this.pos + 1 < len ? src.charCodeAt(this.pos + 1) : 0;
|
|
2988
|
+
if (afterDollar === CH_DQUOTE || afterDollar === CH_SQUOTE) {
|
|
2989
|
+
if (bt) {
|
|
2990
|
+
text += "$";
|
|
2991
|
+
if (bp)
|
|
2992
|
+
litBuf += "$";
|
|
2993
|
+
}
|
|
2994
|
+
this.pos++;
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
const expStart = this.pos;
|
|
2998
|
+
this.readDollar(true);
|
|
2999
|
+
if (bt)
|
|
3000
|
+
text += this._resultText;
|
|
3001
|
+
if (this._resultHasExpansion)
|
|
3002
|
+
hasExpansions = true;
|
|
3003
|
+
if (bp) {
|
|
3004
|
+
const rp = this._resultPart;
|
|
3005
|
+
if (rp && isDQChild(rp)) {
|
|
3006
|
+
if (!parts)
|
|
3007
|
+
parts = [];
|
|
3008
|
+
if (expStart > litStart) {
|
|
3009
|
+
parts.push({
|
|
3010
|
+
type: "Literal",
|
|
3011
|
+
pos: litStart,
|
|
3012
|
+
end: expStart,
|
|
3013
|
+
value: litBuf,
|
|
3014
|
+
text: src.slice(litStart, expStart),
|
|
3015
|
+
});
|
|
3016
|
+
litBuf = "";
|
|
3017
|
+
}
|
|
3018
|
+
parts.push(rp);
|
|
3019
|
+
litStart = this.pos;
|
|
3020
|
+
}
|
|
3021
|
+
else {
|
|
3022
|
+
litBuf += this._resultText;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
if (ch === CH_BACKTICK) {
|
|
3028
|
+
const btStart = this.pos;
|
|
3029
|
+
this.readBacktickExpansion(true);
|
|
3030
|
+
if (bt)
|
|
3031
|
+
text += this._resultText;
|
|
3032
|
+
hasExpansions = true;
|
|
3033
|
+
if (bp && this._resultPart && isDQChild(this._resultPart)) {
|
|
3034
|
+
if (!parts)
|
|
3035
|
+
parts = [];
|
|
3036
|
+
if (btStart > litStart) {
|
|
3037
|
+
parts.push({
|
|
3038
|
+
type: "Literal",
|
|
3039
|
+
pos: litStart,
|
|
3040
|
+
end: btStart,
|
|
3041
|
+
value: litBuf,
|
|
3042
|
+
text: src.slice(litStart, btStart),
|
|
3043
|
+
});
|
|
3044
|
+
litBuf = "";
|
|
3045
|
+
}
|
|
3046
|
+
parts.push(this._resultPart);
|
|
3047
|
+
litStart = this.pos;
|
|
3048
|
+
}
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
if (bp && parts && this.pos > litStart)
|
|
3053
|
+
parts.push({ type: "Literal", pos: litStart, end: this.pos, value: litBuf, text: src.slice(litStart, this.pos) });
|
|
3054
|
+
this._dqEnd = this.pos;
|
|
3055
|
+
if (this.pos < len)
|
|
3056
|
+
this.pos++; // closing "
|
|
3057
|
+
else
|
|
3058
|
+
this.errors.push({ message: "unterminated double quote", pos: contentStart - 1 });
|
|
3059
|
+
this._dqText = text;
|
|
3060
|
+
this._dqHasExpansions = hasExpansions;
|
|
3061
|
+
this._dqParts = parts;
|
|
3062
|
+
}
|
|
3063
|
+
readDollar(insideDoubleQuotes = false) {
|
|
3064
|
+
const dollarPos = this.pos;
|
|
3065
|
+
this.pos++; // skip $
|
|
3066
|
+
const src = this.src;
|
|
3067
|
+
const len = this.srcEnd;
|
|
3068
|
+
const bt = this._buildParts || this._buildValue;
|
|
3069
|
+
if (this.pos >= len) {
|
|
3070
|
+
this._resultText = "$";
|
|
3071
|
+
this._resultIsRaw = true;
|
|
3072
|
+
this._resultHasExpansion = false;
|
|
3073
|
+
this._resultPart = undefined;
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
const logicalPos = skipLineContinuations(src, this.pos, len);
|
|
3077
|
+
if (logicalPos > this.pos &&
|
|
3078
|
+
src.charCodeAt(logicalPos) === CH_LPAREN &&
|
|
3079
|
+
src.charCodeAt(logicalPos + 1) !== CH_LPAREN)
|
|
3080
|
+
this.pos = logicalPos;
|
|
3081
|
+
const ch = src.charCodeAt(this.pos);
|
|
3082
|
+
if (ch === CH_LPAREN) {
|
|
3083
|
+
if (this.pos + 1 < len && src.charCodeAt(this.pos + 1) === CH_LPAREN) {
|
|
3084
|
+
const savedPos = this.pos;
|
|
3085
|
+
const savedErrors = this.errors.length;
|
|
3086
|
+
this.readArithmeticExpansion(dollarPos);
|
|
3087
|
+
if (!this._notArithmetic)
|
|
3088
|
+
return;
|
|
3089
|
+
this.errors.length = savedErrors;
|
|
3090
|
+
this.pos = savedPos;
|
|
3091
|
+
}
|
|
3092
|
+
this.readCommandSubstitution(dollarPos);
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
if (ch === CH_LBRACE) {
|
|
3096
|
+
const after = this.pos + 1 < len ? src.charCodeAt(this.pos + 1) : 0;
|
|
3097
|
+
if (after === CH_SPACE || after === CH_TAB || after === CH_NL) {
|
|
3098
|
+
this.readBraceCommandSubstitution();
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
if (after === CH_PIPE) {
|
|
3102
|
+
this.readValueSubstitution();
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
this.readParameterExpansion(insideDoubleQuotes);
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
if (ch === CH_SQUOTE) {
|
|
3109
|
+
this.pos++;
|
|
3110
|
+
if (bt) {
|
|
3111
|
+
const value = this.readAnsiCQuoted();
|
|
3112
|
+
this._resultText = value;
|
|
3113
|
+
this._resultPart = this._buildParts
|
|
3114
|
+
? { type: "AnsiCQuoted", pos: dollarPos, end: this.pos, text: src.slice(dollarPos, this.pos), value }
|
|
3115
|
+
: undefined;
|
|
3116
|
+
}
|
|
3117
|
+
else {
|
|
3118
|
+
this.skipAnsiCQuoted();
|
|
3119
|
+
this._resultText = "";
|
|
3120
|
+
this._resultPart = undefined;
|
|
3121
|
+
}
|
|
3122
|
+
this._resultIsRaw = false;
|
|
3123
|
+
this._resultHasExpansion = false;
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (ch === CH_DQUOTE) {
|
|
3127
|
+
this.pos++;
|
|
3128
|
+
this.readDoubleQuoted(insideDoubleQuotes);
|
|
3129
|
+
this._resultText = this._dqText;
|
|
3130
|
+
this._resultIsRaw = false;
|
|
3131
|
+
this._resultHasExpansion = this._dqHasExpansions;
|
|
3132
|
+
if (this._buildParts) {
|
|
3133
|
+
const text = src.slice(dollarPos, this.pos);
|
|
3134
|
+
this._resultPart = {
|
|
3135
|
+
type: "LocaleString",
|
|
3136
|
+
pos: dollarPos,
|
|
3137
|
+
end: this.pos,
|
|
3138
|
+
text,
|
|
3139
|
+
parts: this._dqParts ?? [
|
|
3140
|
+
{
|
|
3141
|
+
type: "Literal",
|
|
3142
|
+
pos: dollarPos + 2,
|
|
3143
|
+
end: this._dqEnd,
|
|
3144
|
+
value: this._dqText,
|
|
3145
|
+
text: src.slice(dollarPos + 2, this._dqEnd),
|
|
3146
|
+
},
|
|
3147
|
+
],
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
else {
|
|
3151
|
+
this._resultPart = undefined;
|
|
3152
|
+
}
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
if (ch === CH_AT ||
|
|
3156
|
+
ch === CH_STAR ||
|
|
3157
|
+
ch === CH_HASH ||
|
|
3158
|
+
ch === CH_QUESTION ||
|
|
3159
|
+
ch === CH_DASH ||
|
|
3160
|
+
ch === CH_DOLLAR ||
|
|
3161
|
+
ch === CH_BANG ||
|
|
3162
|
+
(ch >= CH_0 && ch <= CH_9)) {
|
|
3163
|
+
this.pos++;
|
|
3164
|
+
const text = bt ? src.slice(this.pos - 2, this.pos) : "";
|
|
3165
|
+
this._resultText = text;
|
|
3166
|
+
this._resultIsRaw = true;
|
|
3167
|
+
this._resultHasExpansion = false;
|
|
3168
|
+
this._resultPart = this._buildParts
|
|
3169
|
+
? { type: "SimpleExpansion", pos: dollarPos, end: this.pos, text }
|
|
3170
|
+
: undefined;
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
if (ch < 128 && isIdChar[ch] & 1) {
|
|
3174
|
+
const namePos = this.pos - 1;
|
|
3175
|
+
while (this.pos < len) {
|
|
3176
|
+
const c = src.charCodeAt(this.pos);
|
|
3177
|
+
if (c < 128 && isIdChar[c] & 2)
|
|
3178
|
+
this.pos++;
|
|
3179
|
+
else
|
|
3180
|
+
break;
|
|
3181
|
+
}
|
|
3182
|
+
const text = bt ? src.slice(namePos, this.pos) : "";
|
|
3183
|
+
this._resultText = text;
|
|
3184
|
+
this._resultIsRaw = true;
|
|
3185
|
+
this._resultHasExpansion = false;
|
|
3186
|
+
this._resultPart = this._buildParts
|
|
3187
|
+
? { type: "SimpleExpansion", pos: dollarPos, end: this.pos, text }
|
|
3188
|
+
: undefined;
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
// $[ expr ] is bash's deprecated spelling of $(( expr )). It runs to the matching ']',
|
|
3192
|
+
// so parentheses inside belong to the expansion instead of ending the word.
|
|
3193
|
+
if (ch === CH_LBRACKET) {
|
|
3194
|
+
const errorCount = this._errors === null ? 0 : this._errors.length;
|
|
3195
|
+
const close = this.findClosingArithmeticBracket(this.pos + 1);
|
|
3196
|
+
if (close !== -1) {
|
|
3197
|
+
const bodyStart = this.pos + 1;
|
|
3198
|
+
const body = src.slice(bodyStart, close);
|
|
3199
|
+
this.pos = close + 1;
|
|
3200
|
+
const text = bt ? src.slice(dollarPos, this.pos) : "";
|
|
3201
|
+
this._resultText = text;
|
|
3202
|
+
this._resultIsRaw = true;
|
|
3203
|
+
this._resultHasExpansion = false;
|
|
3204
|
+
this._resultPart = this._buildParts
|
|
3205
|
+
? this.arithmeticExpansion(bodyStart - 2, text, body, bodyStart)
|
|
3206
|
+
: undefined;
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
// A failed lookahead leaves `$` literal and the token path re-reads the scanned text.
|
|
3210
|
+
if (this._errors !== null)
|
|
3211
|
+
this._errors.length = errorCount;
|
|
3212
|
+
}
|
|
3213
|
+
this._resultText = "$";
|
|
3214
|
+
this._resultIsRaw = true;
|
|
3215
|
+
this._resultHasExpansion = false;
|
|
3216
|
+
this._resultPart = undefined;
|
|
3217
|
+
}
|
|
3218
|
+
scanArithmeticBody() {
|
|
3219
|
+
this._notArithmetic = false;
|
|
3220
|
+
this._arithUnterminated = false;
|
|
3221
|
+
this.pos += 2;
|
|
3222
|
+
let depth = 1;
|
|
3223
|
+
let parenDepth = 0;
|
|
3224
|
+
let parentParenDepth = 0;
|
|
3225
|
+
let parenDepths;
|
|
3226
|
+
let reported = false;
|
|
3227
|
+
const src = this.src;
|
|
3228
|
+
const len = this.srcEnd;
|
|
3229
|
+
const start = this.pos;
|
|
3230
|
+
while (this.pos < len && depth > 0) {
|
|
3231
|
+
const c = src.charCodeAt(this.pos);
|
|
3232
|
+
if (c === CH_BACKSLASH) {
|
|
3233
|
+
this.pos += this.pos + 1 < len ? 2 : 1;
|
|
3234
|
+
}
|
|
3235
|
+
else if (c === CH_SQUOTE) {
|
|
3236
|
+
this.pos++;
|
|
3237
|
+
this.skipSQ();
|
|
3238
|
+
}
|
|
3239
|
+
else if (c === CH_DQUOTE) {
|
|
3240
|
+
this.pos++;
|
|
3241
|
+
this.skipDQ();
|
|
3242
|
+
}
|
|
3243
|
+
else if (c === CH_BACKTICK) {
|
|
3244
|
+
this.pos++;
|
|
3245
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_BACKTICK) {
|
|
3246
|
+
if (src.charCodeAt(this.pos) === CH_BACKSLASH)
|
|
3247
|
+
this.pos++;
|
|
3248
|
+
this.pos++;
|
|
3249
|
+
}
|
|
3250
|
+
if (this.pos < len)
|
|
3251
|
+
this.pos++;
|
|
3252
|
+
}
|
|
3253
|
+
else if (c === CH_DOLLAR &&
|
|
3254
|
+
this.pos + 2 < len &&
|
|
3255
|
+
src.charCodeAt(this.pos + 1) === CH_LPAREN &&
|
|
3256
|
+
src.charCodeAt(this.pos + 2) !== CH_LPAREN) {
|
|
3257
|
+
const dollarPos = this.pos;
|
|
3258
|
+
this.pos += 2;
|
|
3259
|
+
this.extractBalanced();
|
|
3260
|
+
if (this._unbalanced)
|
|
3261
|
+
this.errors.push({ message: "unterminated command substitution", pos: dollarPos });
|
|
3262
|
+
}
|
|
3263
|
+
else if (c === CH_DOLLAR && this.pos + 1 < len && src.charCodeAt(this.pos + 1) === CH_LBRACE) {
|
|
3264
|
+
const close = this.findClosingBrace(this.pos + 2, len);
|
|
3265
|
+
this.pos = close === -1 ? len : close + 1;
|
|
3266
|
+
}
|
|
3267
|
+
else if ((c === CH_LT || c === CH_GT) && this.pos + 1 < len && src.charCodeAt(this.pos + 1) === CH_LPAREN) {
|
|
3268
|
+
this.pos += 2;
|
|
3269
|
+
this.extractBalanced();
|
|
3270
|
+
}
|
|
3271
|
+
else if (c === CH_LPAREN) {
|
|
3272
|
+
if (src.charCodeAt(this.pos - 1) === CH_DOLLAR && src.charCodeAt(this.pos + 1) === CH_LPAREN) {
|
|
3273
|
+
if (depth === 1)
|
|
3274
|
+
parentParenDepth = parenDepth;
|
|
3275
|
+
else
|
|
3276
|
+
(parenDepths ?? (parenDepths = [])).push(parenDepth);
|
|
3277
|
+
depth++;
|
|
3278
|
+
parenDepth = 0;
|
|
3279
|
+
if (depth + this._nestingDepth > MAX_SYNTAX_NESTING && !reported) {
|
|
3280
|
+
this.errors.push({ message: "maximum arithmetic expansion nesting depth exceeded", pos: this.pos - 1 });
|
|
3281
|
+
reported = true;
|
|
3282
|
+
}
|
|
3283
|
+
this.pos += 2;
|
|
3284
|
+
}
|
|
3285
|
+
else {
|
|
3286
|
+
// The expression parser stops building structure at this depth; report it here, while
|
|
3287
|
+
// scanning, since the expression itself resolves lazily. Each enclosing `$((` of this
|
|
3288
|
+
// scan parses one level deeper, so its parentheses count from there.
|
|
3289
|
+
if (++parenDepth + depth - 1 + this._nestingDepth > MAX_SYNTAX_NESTING && !reported) {
|
|
3290
|
+
this.errors.push({ message: "maximum arithmetic expression nesting depth exceeded", pos: this.pos });
|
|
3291
|
+
reported = true;
|
|
3292
|
+
}
|
|
3293
|
+
this.pos++;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
else if (c === CH_RPAREN && parenDepth > 0) {
|
|
3297
|
+
parenDepth--;
|
|
3298
|
+
this.pos++;
|
|
3299
|
+
}
|
|
3300
|
+
else if (c === CH_RPAREN && this.pos + 1 < len && src.charCodeAt(this.pos + 1) === CH_RPAREN) {
|
|
3301
|
+
if (--depth === 0) {
|
|
3302
|
+
this.pos += 2;
|
|
3303
|
+
break;
|
|
3304
|
+
}
|
|
3305
|
+
parenDepth = depth === 1 ? parentParenDepth : parenDepths.pop();
|
|
3306
|
+
this.pos += 2;
|
|
3307
|
+
}
|
|
3308
|
+
else if (c === CH_RPAREN && depth === 1) {
|
|
3309
|
+
// Bash reads `((` and `$((` as arithmetic only when the inner parenthesis pair is
|
|
3310
|
+
// immediately followed by `)`. It closed without one, so `((a) )` is nested subshells
|
|
3311
|
+
// and `$((a) || (b))` a command substitution; callers re-read from the start.
|
|
3312
|
+
this._notArithmetic = true;
|
|
3313
|
+
return "";
|
|
3314
|
+
}
|
|
3315
|
+
else {
|
|
3316
|
+
this.pos++;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
const bt = this._buildParts || this._buildValue;
|
|
3320
|
+
if (depth > 0) {
|
|
3321
|
+
this._arithUnterminated = true;
|
|
3322
|
+
return bt ? src.slice(start, this.pos) : "";
|
|
3323
|
+
}
|
|
3324
|
+
return bt ? src.slice(start, this.pos - 2) : "";
|
|
3325
|
+
}
|
|
3326
|
+
readArithmeticExpansion(dollarPos) {
|
|
3327
|
+
const bodyStart = this.pos + 2; // absolute offset of the body, past the "((" at this.pos
|
|
3328
|
+
const body = this.scanArithmeticBody();
|
|
3329
|
+
if (this._notArithmetic)
|
|
3330
|
+
return;
|
|
3331
|
+
if (this._arithUnterminated)
|
|
3332
|
+
this.errors.push({ message: "unterminated arithmetic expansion", pos: dollarPos });
|
|
3333
|
+
const text = this._buildParts || this._buildValue ? this.src.slice(dollarPos, this.pos) : "";
|
|
3334
|
+
this._resultText = text;
|
|
3335
|
+
this._resultIsRaw = true;
|
|
3336
|
+
this._resultHasExpansion = false;
|
|
3337
|
+
this._resultPart = this._buildParts ? this.arithmeticExpansion(dollarPos, text, body, bodyStart) : undefined;
|
|
3338
|
+
}
|
|
3339
|
+
arithmeticExpansion(pos, text, body, bodyStart) {
|
|
3340
|
+
return new ArithmeticExpansionImpl(pos, this.pos, text, {
|
|
3341
|
+
source: this.src,
|
|
3342
|
+
body,
|
|
3343
|
+
offset: bodyStart,
|
|
3344
|
+
depth: this._nestingDepth,
|
|
3345
|
+
context: this.substitutionContext,
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
readArithmeticCommand(out, tokenStart) {
|
|
3349
|
+
const savedBuildValue = this._buildValue;
|
|
3350
|
+
this._buildValue = true;
|
|
3351
|
+
const body = this.scanArithmeticBody();
|
|
3352
|
+
this._buildValue = savedBuildValue;
|
|
3353
|
+
if (this._arithUnterminated)
|
|
3354
|
+
this.errors.push({ message: "unterminated arithmetic command", pos: tokenStart });
|
|
3355
|
+
setToken(out, Token.ArithCmd, body, tokenStart, this.pos);
|
|
3356
|
+
}
|
|
3357
|
+
readCommandSubstitution(dollarPos) {
|
|
3358
|
+
const openPos = this.pos;
|
|
3359
|
+
this.pos++; // skip (
|
|
3360
|
+
// extractBalanced returns the inner text without the closing paren, so it
|
|
3361
|
+
// stays correct when the input ended before that paren was reached.
|
|
3362
|
+
const inner = this.extractBalanced();
|
|
3363
|
+
if (this._unbalanced)
|
|
3364
|
+
this.errors.push({ message: "unterminated command substitution", pos: dollarPos });
|
|
3365
|
+
const bt = this._buildParts || this._buildValue;
|
|
3366
|
+
const rawText = bt ? this.src.slice(dollarPos, this.pos) : "";
|
|
3367
|
+
const text = !bt || openPos === dollarPos + 1 ? rawText : "$" + this.src.slice(openPos, this.pos);
|
|
3368
|
+
this._resultText = text;
|
|
3369
|
+
this._resultIsRaw = openPos === dollarPos + 1;
|
|
3370
|
+
this._resultHasExpansion = true;
|
|
3371
|
+
if (this._buildParts) {
|
|
3372
|
+
this._resultPart = this.commandExpansion(dollarPos, rawText, openPos + 1, inner, !this._unbalanced);
|
|
3373
|
+
}
|
|
3374
|
+
else {
|
|
3375
|
+
this._resultPart = undefined;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
readBraceCommandSubstitution() {
|
|
3379
|
+
this.readBraceSubstitution(1);
|
|
3380
|
+
}
|
|
3381
|
+
readValueSubstitution() {
|
|
3382
|
+
this.readBraceSubstitution(2);
|
|
3383
|
+
}
|
|
3384
|
+
readBraceSubstitution(skip) {
|
|
3385
|
+
const dollarPos = this.pos - 1;
|
|
3386
|
+
this.pos += skip;
|
|
3387
|
+
const src = this.src;
|
|
3388
|
+
const len = this.srcEnd;
|
|
3389
|
+
let depth = 1;
|
|
3390
|
+
const start = this.pos;
|
|
3391
|
+
while (this.pos < len) {
|
|
3392
|
+
const c = src.charCodeAt(this.pos);
|
|
3393
|
+
if (c === CH_LBRACE)
|
|
3394
|
+
depth++;
|
|
3395
|
+
else if (c === CH_RBRACE) {
|
|
3396
|
+
if (--depth === 0) {
|
|
3397
|
+
this.pos++;
|
|
3398
|
+
break;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
else if (c === CH_SQUOTE) {
|
|
3402
|
+
this.pos++;
|
|
3403
|
+
this.skipSQ();
|
|
3404
|
+
continue;
|
|
3405
|
+
}
|
|
3406
|
+
else if (c === CH_DQUOTE) {
|
|
3407
|
+
this.pos++;
|
|
3408
|
+
this.skipDQ();
|
|
3409
|
+
continue;
|
|
3410
|
+
}
|
|
3411
|
+
else if (c === CH_BACKSLASH)
|
|
3412
|
+
this.pos++;
|
|
3413
|
+
this.pos++;
|
|
3414
|
+
}
|
|
3415
|
+
this._resultIsRaw = true;
|
|
3416
|
+
this._resultHasExpansion = true;
|
|
3417
|
+
if (this._buildParts || this._buildValue) {
|
|
3418
|
+
const rawInner = src.slice(start, this.pos - 1);
|
|
3419
|
+
const inner = rawInner.trim();
|
|
3420
|
+
const text = src.slice(dollarPos, this.pos);
|
|
3421
|
+
this._resultText = text;
|
|
3422
|
+
if (this._buildParts) {
|
|
3423
|
+
const innerStart = start + (rawInner.length - rawInner.trimStart().length);
|
|
3424
|
+
this._resultPart = this.commandExpansion(dollarPos, text, innerStart, inner);
|
|
3425
|
+
}
|
|
3426
|
+
else {
|
|
3427
|
+
this._resultPart = undefined;
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
else {
|
|
3431
|
+
this._resultText = "";
|
|
3432
|
+
this._resultPart = undefined;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
readBacktickExpansion(insideDoubleQuotes = false) {
|
|
3436
|
+
this.pos++; // skip opening `
|
|
3437
|
+
const src = this.src;
|
|
3438
|
+
const len = this.srcEnd;
|
|
3439
|
+
const start = this.pos;
|
|
3440
|
+
if (!this._buildParts && !this._buildValue) {
|
|
3441
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_BACKTICK) {
|
|
3442
|
+
if (src.charCodeAt(this.pos) === CH_BACKSLASH && this.pos + 1 < len)
|
|
3443
|
+
this.pos++;
|
|
3444
|
+
this.pos++;
|
|
3445
|
+
}
|
|
3446
|
+
if (this.pos < len)
|
|
3447
|
+
this.pos++;
|
|
3448
|
+
else
|
|
3449
|
+
this.errors.push({ message: "unterminated backtick", pos: start - 1 });
|
|
3450
|
+
this._resultText = "";
|
|
3451
|
+
this._resultIsRaw = false;
|
|
3452
|
+
this._resultHasExpansion = true;
|
|
3453
|
+
this._resultPart = undefined;
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
let inner = "";
|
|
3457
|
+
let hasEscapes = false;
|
|
3458
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_BACKTICK) {
|
|
3459
|
+
if (src.charCodeAt(this.pos) === CH_BACKSLASH) {
|
|
3460
|
+
hasEscapes = true;
|
|
3461
|
+
break;
|
|
3462
|
+
}
|
|
3463
|
+
this.pos++;
|
|
3464
|
+
}
|
|
3465
|
+
if (!hasEscapes) {
|
|
3466
|
+
inner = src.slice(start, this.pos);
|
|
3467
|
+
}
|
|
3468
|
+
else {
|
|
3469
|
+
inner = src.slice(start, this.pos);
|
|
3470
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_BACKTICK) {
|
|
3471
|
+
if (src.charCodeAt(this.pos) === CH_BACKSLASH) {
|
|
3472
|
+
this.pos++;
|
|
3473
|
+
if (this.pos < len) {
|
|
3474
|
+
const c = src.charCodeAt(this.pos);
|
|
3475
|
+
if (c === CH_DOLLAR || c === CH_BACKTICK || c === CH_BACKSLASH || (insideDoubleQuotes && c === CH_DQUOTE)) {
|
|
3476
|
+
inner += src[this.pos];
|
|
3477
|
+
}
|
|
3478
|
+
else {
|
|
3479
|
+
inner += "\\" + src[this.pos];
|
|
3480
|
+
}
|
|
3481
|
+
this.pos++;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
else {
|
|
3485
|
+
const runStart = this.pos;
|
|
3486
|
+
while (this.pos < len) {
|
|
3487
|
+
const c = src.charCodeAt(this.pos);
|
|
3488
|
+
if (c === CH_BACKTICK || c === CH_BACKSLASH)
|
|
3489
|
+
break;
|
|
3490
|
+
this.pos++;
|
|
3491
|
+
}
|
|
3492
|
+
inner += src.slice(runStart, this.pos);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
if (this.pos < len)
|
|
3497
|
+
this.pos++; // closing `
|
|
3498
|
+
else
|
|
3499
|
+
this.errors.push({ message: "unterminated backtick", pos: start - 1 });
|
|
3500
|
+
const text = src.slice(start - 1, this.pos); // raw source including backticks
|
|
3501
|
+
this._resultText = inner;
|
|
3502
|
+
this._resultHasExpansion = true;
|
|
3503
|
+
if (this._buildParts) {
|
|
3504
|
+
this._resultPart = hasEscapes
|
|
3505
|
+
? this.decodedCommandExpansion(start - 1, text, inner)
|
|
3506
|
+
: this.commandExpansion(start - 1, text, start, inner);
|
|
3507
|
+
}
|
|
3508
|
+
else {
|
|
3509
|
+
this._resultPart = undefined;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
readParameterExpansion(insideDoubleQuotes) {
|
|
3513
|
+
const src = this.src;
|
|
3514
|
+
const len = this.srcEnd;
|
|
3515
|
+
const start = this.pos; // at {
|
|
3516
|
+
this.pos++;
|
|
3517
|
+
let depth = 1;
|
|
3518
|
+
let reported = false;
|
|
3519
|
+
while (this.pos < len && depth > 0) {
|
|
3520
|
+
const ch = src.charCodeAt(this.pos);
|
|
3521
|
+
if (ch === CH_DOLLAR) {
|
|
3522
|
+
// Dispatch forward: `$$` consumes its own second `$`, so a `{` after it is literal.
|
|
3523
|
+
const next = this.pos + 1 < len ? src.charCodeAt(this.pos + 1) : 0;
|
|
3524
|
+
if (next === CH_LBRACE) {
|
|
3525
|
+
depth++;
|
|
3526
|
+
if (this._nestingDepth + depth > MAX_SYNTAX_NESTING && !reported) {
|
|
3527
|
+
this.errors.push({ message: "maximum parameter expansion nesting depth exceeded", pos: this.pos });
|
|
3528
|
+
reported = true;
|
|
3529
|
+
}
|
|
3530
|
+
this.pos += 2;
|
|
3531
|
+
continue;
|
|
3532
|
+
}
|
|
3533
|
+
if (next === CH_DOLLAR) {
|
|
3534
|
+
this.pos += 2;
|
|
3535
|
+
continue;
|
|
3536
|
+
}
|
|
3537
|
+
if (next === CH_LPAREN) {
|
|
3538
|
+
const dollarPos = this.pos;
|
|
3539
|
+
this.pos += 2;
|
|
3540
|
+
this.extractBalanced();
|
|
3541
|
+
if (this._unbalanced)
|
|
3542
|
+
this.errors.push({ message: "unterminated command substitution", pos: dollarPos });
|
|
3543
|
+
continue;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
else if (ch === CH_BACKTICK) {
|
|
3547
|
+
this.pos++;
|
|
3548
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_BACKTICK) {
|
|
3549
|
+
if (src.charCodeAt(this.pos) === CH_BACKSLASH)
|
|
3550
|
+
this.pos++;
|
|
3551
|
+
this.pos++;
|
|
3552
|
+
}
|
|
3553
|
+
if (this.pos < len)
|
|
3554
|
+
this.pos++;
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
else if (ch === CH_RBRACE) {
|
|
3558
|
+
if (--depth === 0) {
|
|
3559
|
+
this.pos++;
|
|
3560
|
+
break;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
else if (ch === CH_BACKSLASH) {
|
|
3564
|
+
if (this.pos + 1 < len)
|
|
3565
|
+
this.pos++;
|
|
3566
|
+
}
|
|
3567
|
+
else if (ch === CH_SQUOTE) {
|
|
3568
|
+
this.pos++;
|
|
3569
|
+
if (this.pos > start + 1 && src.charCodeAt(this.pos - 2) === CH_DOLLAR)
|
|
3570
|
+
this.skipAnsiCQuoted();
|
|
3571
|
+
else
|
|
3572
|
+
this.skipSQ();
|
|
3573
|
+
continue;
|
|
3574
|
+
}
|
|
3575
|
+
else if (ch === CH_DQUOTE) {
|
|
3576
|
+
this.pos++;
|
|
3577
|
+
this.skipDQ();
|
|
3578
|
+
continue;
|
|
3579
|
+
}
|
|
3580
|
+
this.pos++;
|
|
3581
|
+
}
|
|
3582
|
+
const closed = depth === 0;
|
|
3583
|
+
if (!closed)
|
|
3584
|
+
this.errors.push({ message: "unterminated parameter expansion", pos: start - 1 });
|
|
3585
|
+
const text = this._buildParts || this._buildValue ? src.slice(start - 1, this.pos) : "";
|
|
3586
|
+
this._resultText = text;
|
|
3587
|
+
this._resultIsRaw = true;
|
|
3588
|
+
this._resultHasExpansion = false;
|
|
3589
|
+
if (this._buildParts) {
|
|
3590
|
+
const inner = src.slice(start + 1, closed ? this.pos - 1 : this.pos);
|
|
3591
|
+
this._resultPart = this.parseParamInner(text, inner, start + 1, insideDoubleQuotes);
|
|
3592
|
+
}
|
|
3593
|
+
else {
|
|
3594
|
+
this._resultPart = undefined;
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
// `innerStart` is the absolute offset of `inner` in the original source, so each sub-field
|
|
3598
|
+
// word is parsed in place at its true position. `sub(a, b)` maps inner-relative offsets to
|
|
3599
|
+
// that absolute window.
|
|
3600
|
+
parseParamInner(text, inner, innerStart, insideDoubleQuotes) {
|
|
3601
|
+
const result = {
|
|
3602
|
+
type: "ParameterExpansion",
|
|
3603
|
+
pos: innerStart - 2,
|
|
3604
|
+
end: this.pos,
|
|
3605
|
+
text,
|
|
3606
|
+
parameter: "",
|
|
3607
|
+
parameterPos: innerStart,
|
|
3608
|
+
parameterEnd: innerStart,
|
|
3609
|
+
prefix: undefined,
|
|
3610
|
+
index: undefined,
|
|
3611
|
+
operation: undefined,
|
|
3612
|
+
};
|
|
3613
|
+
const ilen = inner.length;
|
|
3614
|
+
if (ilen === 0)
|
|
3615
|
+
return result;
|
|
3616
|
+
const sub = (a, b, context = "word") => this.parseSubFieldWord(innerStart + a, innerStart + b, context);
|
|
3617
|
+
const closeBracket = (start) => {
|
|
3618
|
+
const close = this.findClosingBracket(innerStart + start, innerStart + ilen);
|
|
3619
|
+
return close === -1 ? -1 : close - innerStart;
|
|
3620
|
+
};
|
|
3621
|
+
let i = 0;
|
|
3622
|
+
if (ilen > 1 && inner.charCodeAt(0) === CH_BANG) {
|
|
3623
|
+
result.prefix = "!";
|
|
3624
|
+
i = 1;
|
|
3625
|
+
}
|
|
3626
|
+
if (ilen > 1 && inner.charCodeAt(0) === CH_HASH) {
|
|
3627
|
+
const nameEnd = this.scanParamName(inner, 1);
|
|
3628
|
+
let parameterEnd = nameEnd;
|
|
3629
|
+
let indexEnd = -1;
|
|
3630
|
+
if (parameterEnd < ilen && inner.charCodeAt(parameterEnd) === CH_LBRACKET) {
|
|
3631
|
+
indexEnd = closeBracket(parameterEnd + 1);
|
|
3632
|
+
if (indexEnd !== -1)
|
|
3633
|
+
parameterEnd = indexEnd + 1;
|
|
3634
|
+
}
|
|
3635
|
+
if (nameEnd > 1 && parameterEnd === ilen) {
|
|
3636
|
+
result.prefix = "#";
|
|
3637
|
+
result.parameter = inner.slice(1, nameEnd);
|
|
3638
|
+
result.parameterPos = innerStart + 1;
|
|
3639
|
+
result.parameterEnd = innerStart + nameEnd;
|
|
3640
|
+
if (indexEnd !== -1)
|
|
3641
|
+
result.index = sub(nameEnd + 1, indexEnd, "embedded");
|
|
3642
|
+
return result;
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
const nameStart = i;
|
|
3646
|
+
i = this.scanParamName(inner, i);
|
|
3647
|
+
result.parameter = inner.slice(nameStart, i);
|
|
3648
|
+
result.parameterPos = innerStart + nameStart;
|
|
3649
|
+
result.parameterEnd = innerStart + i;
|
|
3650
|
+
if (i < ilen && inner.charCodeAt(i) === CH_LBRACKET) {
|
|
3651
|
+
const closeB = closeBracket(i + 1);
|
|
3652
|
+
if (closeB !== -1) {
|
|
3653
|
+
result.index = sub(i + 1, closeB, "embedded");
|
|
3654
|
+
i = closeB + 1;
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
if (i >= ilen)
|
|
3658
|
+
return result;
|
|
3659
|
+
const pos = innerStart + i;
|
|
3660
|
+
const end = innerStart + ilen;
|
|
3661
|
+
const opChar = inner.charCodeAt(i);
|
|
3662
|
+
const nextChar = inner.charCodeAt(i + 1);
|
|
3663
|
+
const defaultChar = inner[opChar === CH_COLON ? i + 1 : i];
|
|
3664
|
+
if (defaultChar === "-" || defaultChar === "=" || defaultChar === "+" || defaultChar === "?") {
|
|
3665
|
+
const operandStart = i + (opChar === CH_COLON ? 2 : 1);
|
|
3666
|
+
result.operation = {
|
|
3667
|
+
type: "Default",
|
|
3668
|
+
pos,
|
|
3669
|
+
end,
|
|
3670
|
+
operatorEnd: innerStart + operandStart,
|
|
3671
|
+
operator: opChar === CH_COLON ? `:${defaultChar}` : defaultChar,
|
|
3672
|
+
operand: sub(operandStart, ilen, insideDoubleQuotes ? "quoted" : "word"),
|
|
3673
|
+
};
|
|
3674
|
+
return result;
|
|
3675
|
+
}
|
|
3676
|
+
if (opChar === CH_COLON) {
|
|
3677
|
+
i++;
|
|
3678
|
+
const sliceStart = innerStart + i;
|
|
3679
|
+
const colonIdx = findUnnested(inner.slice(i), CH_COLON, true, (index, quoted) => {
|
|
3680
|
+
return this.findNestedShellEnd(sliceStart + index, end, quoted) - sliceStart;
|
|
3681
|
+
});
|
|
3682
|
+
result.operation = {
|
|
3683
|
+
type: "Slice",
|
|
3684
|
+
pos,
|
|
3685
|
+
end,
|
|
3686
|
+
operatorEnd: sliceStart,
|
|
3687
|
+
operator: ":",
|
|
3688
|
+
offset: sub(i, colonIdx === -1 ? ilen : i + colonIdx, "embedded"),
|
|
3689
|
+
length: colonIdx === -1 ? undefined : sub(i + colonIdx + 1, ilen, "embedded"),
|
|
3690
|
+
};
|
|
3691
|
+
return result;
|
|
3692
|
+
}
|
|
3693
|
+
if (opChar === CH_HASH || opChar === CH_PERCENT) {
|
|
3694
|
+
const doubled = nextChar === opChar;
|
|
3695
|
+
const operator = opChar === CH_HASH ? (doubled ? "##" : "#") : doubled ? "%%" : "%";
|
|
3696
|
+
const operandStart = i + (doubled ? 2 : 1);
|
|
3697
|
+
result.operation = {
|
|
3698
|
+
type: "Remove",
|
|
3699
|
+
pos,
|
|
3700
|
+
end,
|
|
3701
|
+
operatorEnd: innerStart + operandStart,
|
|
3702
|
+
operator,
|
|
3703
|
+
operand: sub(operandStart, ilen),
|
|
3704
|
+
};
|
|
3705
|
+
return result;
|
|
3706
|
+
}
|
|
3707
|
+
if (opChar === CH_SLASH) {
|
|
3708
|
+
i++;
|
|
3709
|
+
let operator = "/";
|
|
3710
|
+
if (nextChar === CH_SLASH || nextChar === CH_HASH || nextChar === CH_PERCENT) {
|
|
3711
|
+
operator = nextChar === CH_SLASH ? "//" : nextChar === CH_HASH ? "/#" : "/%";
|
|
3712
|
+
i++;
|
|
3713
|
+
}
|
|
3714
|
+
let separatorStart = i;
|
|
3715
|
+
if (nextChar === CH_SLASH) {
|
|
3716
|
+
// After // a leading slash belongs to the pattern; /# and /% allow empty patterns.
|
|
3717
|
+
separatorStart = skipLineContinuations(inner, i, ilen);
|
|
3718
|
+
if (inner.charCodeAt(separatorStart) === CH_SLASH)
|
|
3719
|
+
separatorStart++;
|
|
3720
|
+
}
|
|
3721
|
+
const sepIdx = findUnnested(inner.slice(separatorStart), CH_SLASH);
|
|
3722
|
+
const patternEnd = sepIdx === -1 ? ilen : separatorStart + sepIdx;
|
|
3723
|
+
result.operation = {
|
|
3724
|
+
type: "Replace",
|
|
3725
|
+
pos,
|
|
3726
|
+
end,
|
|
3727
|
+
operatorEnd: innerStart + i,
|
|
3728
|
+
operator,
|
|
3729
|
+
pattern: sub(i, patternEnd),
|
|
3730
|
+
replacement: sub(sepIdx === -1 ? ilen : patternEnd + 1, ilen),
|
|
3731
|
+
};
|
|
3732
|
+
return result;
|
|
3733
|
+
}
|
|
3734
|
+
if (opChar === CH_CARET || opChar === CH_COMMA) {
|
|
3735
|
+
const doubled = nextChar === opChar;
|
|
3736
|
+
const operator = opChar === CH_CARET ? (doubled ? "^^" : "^") : doubled ? ",," : ",";
|
|
3737
|
+
const operandStart = i + (doubled ? 2 : 1);
|
|
3738
|
+
result.operation = {
|
|
3739
|
+
type: "CaseModification",
|
|
3740
|
+
pos,
|
|
3741
|
+
end,
|
|
3742
|
+
operatorEnd: innerStart + operandStart,
|
|
3743
|
+
operator,
|
|
3744
|
+
operand: operandStart === ilen ? undefined : sub(operandStart, ilen),
|
|
3745
|
+
};
|
|
3746
|
+
return result;
|
|
3747
|
+
}
|
|
3748
|
+
if (result.prefix === "!" &&
|
|
3749
|
+
result.index === undefined &&
|
|
3750
|
+
i + 1 === ilen &&
|
|
3751
|
+
(opChar === CH_AT || opChar === CH_STAR)) {
|
|
3752
|
+
result.operation = { type: "Names", pos, end, operatorEnd: end, operator: opChar === CH_AT ? "@" : "*" };
|
|
3753
|
+
return result;
|
|
3754
|
+
}
|
|
3755
|
+
if (opChar === CH_AT) {
|
|
3756
|
+
result.operation = {
|
|
3757
|
+
type: "Transform",
|
|
3758
|
+
pos,
|
|
3759
|
+
end,
|
|
3760
|
+
operatorEnd: pos + 1,
|
|
3761
|
+
operator: "@",
|
|
3762
|
+
operand: sub(i + 1, ilen),
|
|
3763
|
+
};
|
|
3764
|
+
return result;
|
|
3765
|
+
}
|
|
3766
|
+
result.operation = { type: "Unknown", pos, end, operatorEnd: end, operator: inner.slice(i) };
|
|
3767
|
+
return result;
|
|
3768
|
+
}
|
|
3769
|
+
findNestedShellEnd(start, end, quoted) {
|
|
3770
|
+
const ch = this.src.charCodeAt(start);
|
|
3771
|
+
if (ch === CH_BACKTICK) {
|
|
3772
|
+
let pos = start + 1;
|
|
3773
|
+
while (pos < end) {
|
|
3774
|
+
const current = this.src.charCodeAt(pos);
|
|
3775
|
+
if (current === CH_BACKSLASH) {
|
|
3776
|
+
pos += 2;
|
|
3777
|
+
continue;
|
|
3778
|
+
}
|
|
3779
|
+
pos++;
|
|
3780
|
+
if (current === CH_BACKTICK)
|
|
3781
|
+
return pos;
|
|
3782
|
+
}
|
|
3783
|
+
return end;
|
|
3784
|
+
}
|
|
3785
|
+
let close;
|
|
3786
|
+
if (ch === CH_DOLLAR) {
|
|
3787
|
+
const next = start + 1;
|
|
3788
|
+
if (next < end && this.src.charCodeAt(next) === CH_LBRACE) {
|
|
3789
|
+
close = this.findClosingBrace(next + 1, end);
|
|
3790
|
+
}
|
|
3791
|
+
else {
|
|
3792
|
+
const open = skipLineContinuations(this.src, next, end);
|
|
3793
|
+
if (open >= end || this.src.charCodeAt(open) !== CH_LPAREN)
|
|
3794
|
+
return start;
|
|
3795
|
+
close = this.findClosingParenthesis(open + 1, end);
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
else {
|
|
3799
|
+
const open = start + 1;
|
|
3800
|
+
if (quoted || (ch !== CH_LT && ch !== CH_GT) || open >= end || this.src.charCodeAt(open) !== CH_LPAREN) {
|
|
3801
|
+
return start;
|
|
3802
|
+
}
|
|
3803
|
+
close = this.findClosingParenthesis(open + 1, end);
|
|
3804
|
+
}
|
|
3805
|
+
return close === -1 ? end : close + 1;
|
|
3806
|
+
}
|
|
3807
|
+
scanParamName(s, start) {
|
|
3808
|
+
let i = start;
|
|
3809
|
+
if (i >= s.length)
|
|
3810
|
+
return i;
|
|
3811
|
+
const c = s.charCodeAt(i);
|
|
3812
|
+
// Special single-char params
|
|
3813
|
+
if (c === CH_AT ||
|
|
3814
|
+
c === CH_STAR ||
|
|
3815
|
+
c === CH_HASH ||
|
|
3816
|
+
c === CH_QUESTION ||
|
|
3817
|
+
c === CH_DASH ||
|
|
3818
|
+
c === CH_DOLLAR ||
|
|
3819
|
+
c === CH_BANG) {
|
|
3820
|
+
return i + 1;
|
|
3821
|
+
}
|
|
3822
|
+
// Digits
|
|
3823
|
+
if (c >= CH_0 && c <= CH_9) {
|
|
3824
|
+
while (i < s.length && s.charCodeAt(i) >= CH_0 && s.charCodeAt(i) <= CH_9)
|
|
3825
|
+
i++;
|
|
3826
|
+
return i;
|
|
3827
|
+
}
|
|
3828
|
+
// Regular name: [a-zA-Z_][a-zA-Z0-9_]*
|
|
3829
|
+
if ((c >= CH_a && c <= CH_z) || (c >= CH_A && c <= CH_Z) || c === CH_UNDERSCORE) {
|
|
3830
|
+
i++;
|
|
3831
|
+
while (i < s.length) {
|
|
3832
|
+
const ch = s.charCodeAt(i);
|
|
3833
|
+
if ((ch >= CH_a && ch <= CH_z) ||
|
|
3834
|
+
(ch >= CH_A && ch <= CH_Z) ||
|
|
3835
|
+
(ch >= CH_0 && ch <= CH_9) ||
|
|
3836
|
+
ch === CH_UNDERSCORE)
|
|
3837
|
+
i++;
|
|
3838
|
+
else
|
|
3839
|
+
break;
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
return i;
|
|
3843
|
+
}
|
|
3844
|
+
readAnsiCQuoted() {
|
|
3845
|
+
const quotePos = this.pos - 1;
|
|
3846
|
+
const result = decodeAnsiCQuoted(this.src, this.pos, this.srcEnd);
|
|
3847
|
+
this.pos = result.end;
|
|
3848
|
+
if (!result.closed)
|
|
3849
|
+
this.errors.push({ message: "unterminated ANSI-C quote", pos: quotePos });
|
|
3850
|
+
return result.value;
|
|
3851
|
+
}
|
|
3852
|
+
// Quotes resume when a nested substitution returns to their parenthesis depth.
|
|
3853
|
+
extractBalanced(useGrammar = true, quoteOnly = false) {
|
|
3854
|
+
const src = this.src;
|
|
3855
|
+
const len = this.srcEnd;
|
|
3856
|
+
const bt = this._buildParts || this._buildValue;
|
|
3857
|
+
let depth = quoteOnly ? 0 : 1;
|
|
3858
|
+
const start = this.pos;
|
|
3859
|
+
this._unbalanced = false;
|
|
3860
|
+
const knownEnd = useGrammar && !quoteOnly ? this.substitutionContext.boundaries?.get(start) : undefined;
|
|
3861
|
+
if (knownEnd !== undefined && knownEnd <= len && this._nestingDepth < MAX_SYNTAX_NESTING) {
|
|
3862
|
+
this.pos = knownEnd;
|
|
3863
|
+
return bt ? src.slice(start, knownEnd - 1) : "";
|
|
3864
|
+
}
|
|
3865
|
+
let wordStart = true;
|
|
3866
|
+
// Fast path: scan for simple cases with no nested quotes/parens/case. Nothing here
|
|
3867
|
+
// opens a paren — `(` breaks to the slow path — so depth stays 1 and the first `)`
|
|
3868
|
+
// closes the construct.
|
|
3869
|
+
while (!quoteOnly && this.pos < len) {
|
|
3870
|
+
const c = src.charCodeAt(this.pos);
|
|
3871
|
+
if (c === CH_RPAREN) {
|
|
3872
|
+
const result = bt ? src.slice(start, this.pos) : "";
|
|
3873
|
+
this.pos++;
|
|
3874
|
+
return result;
|
|
3875
|
+
}
|
|
3876
|
+
else if (c === CH_LPAREN ||
|
|
3877
|
+
c === CH_BACKSLASH ||
|
|
3878
|
+
c === CH_SQUOTE ||
|
|
3879
|
+
c === CH_DQUOTE ||
|
|
3880
|
+
c === CH_BACKTICK ||
|
|
3881
|
+
(c === CH_DOLLAR && (src.charCodeAt(this.pos + 1) === CH_SQUOTE || src.charCodeAt(this.pos + 1) === CH_DOLLAR))) {
|
|
3882
|
+
break;
|
|
3883
|
+
}
|
|
3884
|
+
else if (c === CH_LT && this.pos + 1 < len && src.charCodeAt(this.pos + 1) === CH_LT) {
|
|
3885
|
+
break;
|
|
3886
|
+
}
|
|
3887
|
+
else if (c === CH_HASH && wordStart) {
|
|
3888
|
+
break;
|
|
3889
|
+
}
|
|
3890
|
+
else if (c === 99 /* c */ &&
|
|
3891
|
+
wordStart &&
|
|
3892
|
+
this.pos + 3 < len &&
|
|
3893
|
+
src.charCodeAt(this.pos + 1) === 97 /* a */ &&
|
|
3894
|
+
src.charCodeAt(this.pos + 2) === 115 /* s */ &&
|
|
3895
|
+
src.charCodeAt(this.pos + 3) === 101 /* e */ &&
|
|
3896
|
+
(this.pos + 4 >= len || (src.charCodeAt(this.pos + 4) < 128 && charType[src.charCodeAt(this.pos + 4)] & 1))) {
|
|
3897
|
+
break;
|
|
3898
|
+
}
|
|
3899
|
+
else {
|
|
3900
|
+
wordStart = c < 128 && (charType[c] & 1) !== 0;
|
|
3901
|
+
this.pos++;
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
// Slow path: just track position (source is copied verbatim, so slice at end)
|
|
3905
|
+
let caseDepth = 0;
|
|
3906
|
+
// Parentheses opened since the innermost `case` began. A `)` only terminates a case
|
|
3907
|
+
// pattern when nothing is open — otherwise it closes a pattern's leading `(`, an
|
|
3908
|
+
// extglob group, or a subshell in the case body, and must rebalance `depth`.
|
|
3909
|
+
let caseParens = 0;
|
|
3910
|
+
// Word-form parens resume mid-word; grammar parens resume where `#` can open a comment.
|
|
3911
|
+
const wordStartAfterParen = [];
|
|
3912
|
+
let pendingDelims = null;
|
|
3913
|
+
let scripts;
|
|
3914
|
+
let arithBase = -1;
|
|
3915
|
+
// A `$((` extent is found by bash's arithmetic scanner, where `#` is ordinary, and keeps
|
|
3916
|
+
// that extent even for a command body. The two chars before `start` are the delimiter.
|
|
3917
|
+
let arithExtent = start >= 2 &&
|
|
3918
|
+
src.charCodeAt(start) === CH_LPAREN &&
|
|
3919
|
+
src.charCodeAt(start - 1) === CH_LPAREN &&
|
|
3920
|
+
src.charCodeAt(start - 2) === CH_DOLLAR;
|
|
3921
|
+
let substitutions = 0;
|
|
3922
|
+
let reported = false;
|
|
3923
|
+
const quotes = quoteOnly ? [{ close: CH_DQUOTE, depth: 0 }] : [];
|
|
3924
|
+
let quotedDepth = quoteOnly ? 1 : 0;
|
|
3925
|
+
let parameterDepth = 0;
|
|
3926
|
+
let arithmeticOpens;
|
|
3927
|
+
let continuedDollarPos = -1;
|
|
3928
|
+
let continuedParenPos = -1;
|
|
3929
|
+
let escapedEnd = -1;
|
|
3930
|
+
let lastParenEnd = -1;
|
|
3931
|
+
let checkedGrammar = false;
|
|
3932
|
+
while (this.pos < len && (depth > 0 || quotes.length > 0)) {
|
|
3933
|
+
let ch = src.charCodeAt(this.pos);
|
|
3934
|
+
// A negative index would leave V8's fast array path, so guard the empty stack.
|
|
3935
|
+
const quote = quotes.length === 0 ? undefined : quotes[quotes.length - 1];
|
|
3936
|
+
if (quote !== undefined && quote.depth === depth) {
|
|
3937
|
+
let commandOpen = false;
|
|
3938
|
+
if (ch === CH_BACKSLASH) {
|
|
3939
|
+
this.pos = Math.min(this.pos + 2, len);
|
|
3940
|
+
continue;
|
|
3941
|
+
}
|
|
3942
|
+
if (ch === quote.close) {
|
|
3943
|
+
quotes.pop();
|
|
3944
|
+
if (ch === CH_DQUOTE)
|
|
3945
|
+
quotedDepth--;
|
|
3946
|
+
else
|
|
3947
|
+
parameterDepth--;
|
|
3948
|
+
this.pos++;
|
|
3949
|
+
if (quoteOnly && depth === 0 && quotes.length === 0)
|
|
3950
|
+
return "";
|
|
3951
|
+
continue;
|
|
3952
|
+
}
|
|
3953
|
+
if (ch === CH_DQUOTE) {
|
|
3954
|
+
quotes.push({ close: CH_DQUOTE, depth });
|
|
3955
|
+
quotedDepth++;
|
|
3956
|
+
}
|
|
3957
|
+
else if (ch === CH_SQUOTE && quote.close === CH_RBRACE) {
|
|
3958
|
+
this.pos++;
|
|
3959
|
+
this.skipSQ();
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
else if (ch === CH_BACKTICK) {
|
|
3963
|
+
this.pos++;
|
|
3964
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_BACKTICK) {
|
|
3965
|
+
if (src.charCodeAt(this.pos) === CH_BACKSLASH)
|
|
3966
|
+
this.pos++;
|
|
3967
|
+
if (this.pos < len)
|
|
3968
|
+
this.pos++;
|
|
3969
|
+
}
|
|
3970
|
+
if (this.pos < len)
|
|
3971
|
+
this.pos++;
|
|
3972
|
+
continue;
|
|
3973
|
+
}
|
|
3974
|
+
else if (ch === CH_DOLLAR) {
|
|
3975
|
+
const next = src.charCodeAt(this.pos + 1);
|
|
3976
|
+
if (next === CH_LBRACE) {
|
|
3977
|
+
quotes.push({ close: CH_RBRACE, depth });
|
|
3978
|
+
parameterDepth++;
|
|
3979
|
+
this.pos++;
|
|
3980
|
+
}
|
|
3981
|
+
else if (next === CH_LPAREN) {
|
|
3982
|
+
this.pos++;
|
|
3983
|
+
ch = CH_LPAREN;
|
|
3984
|
+
commandOpen = true;
|
|
3985
|
+
}
|
|
3986
|
+
else if (next === CH_DOLLAR) {
|
|
3987
|
+
// `$$` is one parameter unless `(` follows: Bash's quote scanner then reads `$` + `$(`.
|
|
3988
|
+
this.pos += src.charCodeAt(this.pos + 2) === CH_LPAREN ? 1 : 2;
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
if (!reported && this._nestingDepth + Math.max(quotedDepth, parameterDepth) > MAX_SYNTAX_NESTING) {
|
|
3993
|
+
this.errors.push({ message: "maximum word nesting depth exceeded", pos: this.pos });
|
|
3994
|
+
reported = true;
|
|
3995
|
+
}
|
|
3996
|
+
if (!commandOpen) {
|
|
3997
|
+
// Plain text changes no depth, so the rest of the run needs no further checks.
|
|
3998
|
+
this.pos++;
|
|
3999
|
+
while (this.pos < len) {
|
|
4000
|
+
const c = src.charCodeAt(this.pos);
|
|
4001
|
+
if (c === CH_BACKSLASH ||
|
|
4002
|
+
c === CH_DQUOTE ||
|
|
4003
|
+
c === CH_DOLLAR ||
|
|
4004
|
+
c === CH_BACKTICK ||
|
|
4005
|
+
c === CH_RBRACE ||
|
|
4006
|
+
c === CH_SQUOTE)
|
|
4007
|
+
break;
|
|
4008
|
+
this.pos++;
|
|
4009
|
+
}
|
|
4010
|
+
continue;
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
// The grammar resolves whether '#' after ')' is a comment, including inside array subscripts.
|
|
4014
|
+
if (ch === CH_HASH &&
|
|
4015
|
+
lastParenEnd !== -1 &&
|
|
4016
|
+
!arithExtent &&
|
|
4017
|
+
arithBase < 0 &&
|
|
4018
|
+
useGrammar &&
|
|
4019
|
+
!checkedGrammar &&
|
|
4020
|
+
this._nestingDepth + (scripts?.length ?? 0) < MAX_SYNTAX_NESTING &&
|
|
4021
|
+
skipBlanks(src, lastParenEnd, len) === this.pos) {
|
|
4022
|
+
checkedGrammar = true;
|
|
4023
|
+
const scope = scripts?.[scripts.length - 1];
|
|
4024
|
+
const end = this.findSubstitutionEnd(scope?.start ?? start, len, this._nestingDepth + (scripts?.length ?? 0) + 1);
|
|
4025
|
+
if (end !== -1) {
|
|
4026
|
+
if (scope === undefined) {
|
|
4027
|
+
this.pos = end;
|
|
4028
|
+
return bt ? src.slice(start, end - 1) : "";
|
|
4029
|
+
}
|
|
4030
|
+
// Rejoin the closing-parenthesis path so the proven script end restores its enclosing scope.
|
|
4031
|
+
this.pos = end - 1;
|
|
4032
|
+
depth = scope.depth + 1;
|
|
4033
|
+
caseDepth = 0;
|
|
4034
|
+
caseParens = 0;
|
|
4035
|
+
wordStartAfterParen.length = scope.parens + 1;
|
|
4036
|
+
ch = CH_RPAREN;
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
if (ch === CH_LPAREN) {
|
|
4040
|
+
// An escaped character (`\$(`) is text, not a substitution or extglob prefix.
|
|
4041
|
+
const prev = this.pos > start && this.pos !== escapedEnd ? src.charCodeAt(this.pos - 1) : 0;
|
|
4042
|
+
const commandDollarPos = prev === CH_DOLLAR ? this.pos - 1 : this.pos === continuedParenPos ? continuedDollarPos : -1;
|
|
4043
|
+
const wordParen = commandDollarPos !== -1 ||
|
|
4044
|
+
prev === CH_LT ||
|
|
4045
|
+
prev === CH_GT ||
|
|
4046
|
+
prev === CH_EQ ||
|
|
4047
|
+
prev === CH_AT ||
|
|
4048
|
+
prev === CH_QUESTION ||
|
|
4049
|
+
prev === CH_STAR ||
|
|
4050
|
+
prev === CH_PLUS ||
|
|
4051
|
+
prev === CH_BANG ||
|
|
4052
|
+
(prev === CH_LPAREN && wordStartAfterParen[wordStartAfterParen.length - 1] === false) ||
|
|
4053
|
+
(arithExtent && this.pos === start);
|
|
4054
|
+
const script = (commandDollarPos !== -1 && src.charCodeAt(this.pos + 1) !== CH_LPAREN) ||
|
|
4055
|
+
((prev === CH_LT || prev === CH_GT) && arithBase < 0 && !arithExtent);
|
|
4056
|
+
if (script) {
|
|
4057
|
+
(scripts ?? (scripts = [])).push({
|
|
4058
|
+
start: this.pos + 1,
|
|
4059
|
+
dollar: commandDollarPos,
|
|
4060
|
+
depth,
|
|
4061
|
+
parens: wordStartAfterParen.length,
|
|
4062
|
+
caseDepth,
|
|
4063
|
+
caseParens,
|
|
4064
|
+
arithBase,
|
|
4065
|
+
arithExtent,
|
|
4066
|
+
substitutions,
|
|
4067
|
+
checkedGrammar,
|
|
4068
|
+
pendingDelims,
|
|
4069
|
+
});
|
|
4070
|
+
caseDepth = 0;
|
|
4071
|
+
caseParens = 0;
|
|
4072
|
+
arithBase = -1;
|
|
4073
|
+
arithExtent = false;
|
|
4074
|
+
checkedGrammar = false;
|
|
4075
|
+
pendingDelims = null;
|
|
4076
|
+
}
|
|
4077
|
+
wordStartAfterParen.push(!wordParen);
|
|
4078
|
+
wordStart = true;
|
|
4079
|
+
// (( opens arithmetic — suppress heredoc detection until the parens
|
|
4080
|
+
// rebalance so shift operators inside aren't mistaken for heredocs
|
|
4081
|
+
if (!script && arithBase < 0 && this.pos + 1 < len && src.charCodeAt(this.pos + 1) === CH_LPAREN) {
|
|
4082
|
+
arithBase = depth;
|
|
4083
|
+
}
|
|
4084
|
+
if (commandDollarPos !== -1) {
|
|
4085
|
+
if (script)
|
|
4086
|
+
substitutions++;
|
|
4087
|
+
else
|
|
4088
|
+
(arithmeticOpens ?? (arithmeticOpens = [])).push(depth);
|
|
4089
|
+
if (substitutions + (arithmeticOpens?.length ?? 0) + this._nestingDepth + (quoteOnly ? 0 : 1) >
|
|
4090
|
+
MAX_SYNTAX_NESTING &&
|
|
4091
|
+
!reported) {
|
|
4092
|
+
this.errors.push({ message: "maximum command substitution nesting depth exceeded", pos: commandDollarPos });
|
|
4093
|
+
reported = true;
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
continuedDollarPos = -1;
|
|
4097
|
+
continuedParenPos = -1;
|
|
4098
|
+
depth++;
|
|
4099
|
+
if (caseDepth > 0)
|
|
4100
|
+
caseParens++;
|
|
4101
|
+
this.pos++;
|
|
4102
|
+
}
|
|
4103
|
+
else if (ch === CH_RPAREN) {
|
|
4104
|
+
if (caseDepth > 0 && caseParens === 0) {
|
|
4105
|
+
this.pos++;
|
|
4106
|
+
wordStart = true;
|
|
4107
|
+
}
|
|
4108
|
+
else {
|
|
4109
|
+
if (caseDepth > 0)
|
|
4110
|
+
caseParens--;
|
|
4111
|
+
depth--;
|
|
4112
|
+
if (depth === 0 && !quoteOnly) {
|
|
4113
|
+
const result = bt ? src.slice(start, this.pos) : "";
|
|
4114
|
+
this.pos++;
|
|
4115
|
+
return result;
|
|
4116
|
+
}
|
|
4117
|
+
if (depth <= arithBase)
|
|
4118
|
+
arithBase = -1;
|
|
4119
|
+
if (arithmeticOpens !== undefined) {
|
|
4120
|
+
while (arithmeticOpens.length > 0 && arithmeticOpens[arithmeticOpens.length - 1] >= depth)
|
|
4121
|
+
arithmeticOpens.pop();
|
|
4122
|
+
}
|
|
4123
|
+
wordStart = wordStartAfterParen.pop() ?? true;
|
|
4124
|
+
this.pos++;
|
|
4125
|
+
const scope = scripts?.[scripts.length - 1];
|
|
4126
|
+
if (scope !== undefined && scope.depth === depth) {
|
|
4127
|
+
scripts.pop();
|
|
4128
|
+
caseDepth = scope.caseDepth;
|
|
4129
|
+
caseParens = scope.caseParens;
|
|
4130
|
+
arithBase = scope.arithBase;
|
|
4131
|
+
arithExtent = scope.arithExtent;
|
|
4132
|
+
substitutions = scope.substitutions;
|
|
4133
|
+
checkedGrammar = scope.checkedGrammar;
|
|
4134
|
+
pendingDelims = scope.pendingDelims;
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
lastParenEnd = this.pos;
|
|
4138
|
+
}
|
|
4139
|
+
else if (ch === CH_BACKSLASH) {
|
|
4140
|
+
if (this.pos > start && src.charCodeAt(this.pos - 1) === CH_DOLLAR) {
|
|
4141
|
+
const logicalPos = skipLineContinuations(src, this.pos, len);
|
|
4142
|
+
if (logicalPos > this.pos &&
|
|
4143
|
+
src.charCodeAt(logicalPos) === CH_LPAREN &&
|
|
4144
|
+
src.charCodeAt(logicalPos + 1) !== CH_LPAREN) {
|
|
4145
|
+
continuedDollarPos = this.pos - 1;
|
|
4146
|
+
continuedParenPos = logicalPos;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
this.pos++;
|
|
4150
|
+
if (this.pos < len) {
|
|
4151
|
+
if (src.charCodeAt(this.pos) !== CH_NL) {
|
|
4152
|
+
wordStart = false;
|
|
4153
|
+
}
|
|
4154
|
+
this.pos++;
|
|
4155
|
+
}
|
|
4156
|
+
escapedEnd = this.pos;
|
|
4157
|
+
}
|
|
4158
|
+
else if (ch === CH_DOLLAR) {
|
|
4159
|
+
const next = src.charCodeAt(++this.pos);
|
|
4160
|
+
if (next === CH_SQUOTE) {
|
|
4161
|
+
this.pos++;
|
|
4162
|
+
this.skipAnsiCQuoted();
|
|
4163
|
+
}
|
|
4164
|
+
else if (next === CH_DOLLAR) {
|
|
4165
|
+
this.pos++;
|
|
4166
|
+
}
|
|
4167
|
+
wordStart = false;
|
|
4168
|
+
}
|
|
4169
|
+
else if (ch === CH_SQUOTE) {
|
|
4170
|
+
this.pos++;
|
|
4171
|
+
this.skipSQ();
|
|
4172
|
+
wordStart = false;
|
|
4173
|
+
}
|
|
4174
|
+
else if (ch === CH_DQUOTE) {
|
|
4175
|
+
quotes.push({ close: CH_DQUOTE, depth });
|
|
4176
|
+
quotedDepth++;
|
|
4177
|
+
this.pos++;
|
|
4178
|
+
wordStart = false;
|
|
4179
|
+
}
|
|
4180
|
+
else if (ch === CH_BACKTICK) {
|
|
4181
|
+
this.pos++;
|
|
4182
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_BACKTICK) {
|
|
4183
|
+
if (src.charCodeAt(this.pos) === CH_BACKSLASH)
|
|
4184
|
+
this.pos++;
|
|
4185
|
+
if (this.pos < len)
|
|
4186
|
+
this.pos++;
|
|
4187
|
+
}
|
|
4188
|
+
if (this.pos < len)
|
|
4189
|
+
this.pos++;
|
|
4190
|
+
wordStart = false;
|
|
4191
|
+
}
|
|
4192
|
+
else if (ch === CH_LT && arithBase < 0 && this.pos + 1 < len && src.charCodeAt(this.pos + 1) === CH_LT) {
|
|
4193
|
+
// << is a heredoc operator even in case-pattern position — unquoted <<
|
|
4194
|
+
// inside a pattern is a bash syntax error, so no caseDepth gate here
|
|
4195
|
+
if (this.pos + 2 < len && src.charCodeAt(this.pos + 2) === CH_LT) {
|
|
4196
|
+
this.pos += 3; // <<< herestring
|
|
4197
|
+
}
|
|
4198
|
+
else {
|
|
4199
|
+
this.pos += 2;
|
|
4200
|
+
const strip = this.pos < len && src.charCodeAt(this.pos) === CH_DASH;
|
|
4201
|
+
if (strip)
|
|
4202
|
+
this.pos++;
|
|
4203
|
+
this.skipSpacesAndTabs();
|
|
4204
|
+
this.readHereDocDelimiter();
|
|
4205
|
+
// Empty unquoted delimiter means there was no delimiter word (`<< )`)
|
|
4206
|
+
if (this._hereDelim || this._hereQuoted) {
|
|
4207
|
+
(pendingDelims ?? (pendingDelims = [])).push({
|
|
4208
|
+
delimiter: this._hereDelim,
|
|
4209
|
+
strip,
|
|
4210
|
+
quoted: this._hereQuoted,
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
wordStart = false;
|
|
4215
|
+
}
|
|
4216
|
+
else if (ch === CH_NL && pendingDelims) {
|
|
4217
|
+
this.pos++;
|
|
4218
|
+
for (const hd of pendingDelims)
|
|
4219
|
+
this.skipHereDocBody(hd.delimiter, hd.strip, true, hd.quoted);
|
|
4220
|
+
pendingDelims = null;
|
|
4221
|
+
wordStart = true;
|
|
4222
|
+
}
|
|
4223
|
+
else if (ch === CH_HASH && arithBase < 0 && !arithExtent && wordStart) {
|
|
4224
|
+
// Word-boundary # opens a comment — opaque up to (not including) the
|
|
4225
|
+
// newline, so quotes and << inside it stay inert
|
|
4226
|
+
while (this.pos < len && src.charCodeAt(this.pos) !== CH_NL)
|
|
4227
|
+
this.pos++;
|
|
4228
|
+
}
|
|
4229
|
+
else {
|
|
4230
|
+
const wStart = this.pos;
|
|
4231
|
+
while (this.pos < len) {
|
|
4232
|
+
const wc = src.charCodeAt(this.pos);
|
|
4233
|
+
if (wc < 128 && charType[wc])
|
|
4234
|
+
break;
|
|
4235
|
+
this.pos++;
|
|
4236
|
+
}
|
|
4237
|
+
if (this.pos > wStart) {
|
|
4238
|
+
const wLen = this.pos - wStart;
|
|
4239
|
+
if (wLen === 4 && wordStart && arithBase < 0 && !arithExtent) {
|
|
4240
|
+
const c0 = src.charCodeAt(wStart);
|
|
4241
|
+
if (c0 === 99 &&
|
|
4242
|
+
src.charCodeAt(wStart + 1) === 97 &&
|
|
4243
|
+
src.charCodeAt(wStart + 2) === 115 &&
|
|
4244
|
+
src.charCodeAt(wStart + 3) === 101) {
|
|
4245
|
+
caseDepth++;
|
|
4246
|
+
}
|
|
4247
|
+
else if (c0 === 101 &&
|
|
4248
|
+
src.charCodeAt(wStart + 1) === 115 &&
|
|
4249
|
+
src.charCodeAt(wStart + 2) === 97 &&
|
|
4250
|
+
src.charCodeAt(wStart + 3) === 99 &&
|
|
4251
|
+
caseDepth > 0) {
|
|
4252
|
+
caseDepth--;
|
|
4253
|
+
if (caseDepth === 0)
|
|
4254
|
+
caseParens = 0;
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
wordStart = false;
|
|
4258
|
+
}
|
|
4259
|
+
else {
|
|
4260
|
+
const wc = src.charCodeAt(this.pos);
|
|
4261
|
+
wordStart = wc < 128 && (charType[wc] & 1) !== 0;
|
|
4262
|
+
this.pos++;
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
// Ran out of input before the closing paren. A quoted scan owns the `$(` it opened: the
|
|
4267
|
+
// outermost one left open is reported here, since no token read revisits this text.
|
|
4268
|
+
this._unbalanced = true;
|
|
4269
|
+
if (quoteOnly && scripts !== undefined && scripts.length > 0) {
|
|
4270
|
+
this.errors.push({ message: "unterminated command substitution", pos: scripts[0].dollar });
|
|
4271
|
+
}
|
|
4272
|
+
return bt ? src.slice(start, this.pos) : "";
|
|
4273
|
+
}
|
|
4274
|
+
}
|