wave-agent-sdk 1.1.4 → 1.2.0
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/dist/agent.d.ts +72 -24
- package/dist/agent.js +123 -31
- package/dist/builtin/plugins.js +17 -2
- package/dist/builtin/skills/settings.js +6 -8
- package/dist/core/plugin.d.ts +4 -0
- package/dist/core/plugin.js +7 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/managers/aiManager.d.ts +0 -24
- package/dist/managers/aiManager.js +23 -181
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +5 -0
- package/dist/managers/hookManager.js +7 -0
- package/dist/managers/liveConfigManager.js +3 -3
- package/dist/managers/mcpManager.d.ts +23 -0
- package/dist/managers/mcpManager.js +162 -14
- package/dist/managers/messageManager.d.ts +12 -13
- package/dist/managers/messageManager.js +63 -60
- package/dist/managers/permissionManager.d.ts +29 -0
- package/dist/managers/permissionManager.js +158 -70
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- package/dist/managers/skillManager.d.ts +31 -0
- package/dist/managers/skillManager.js +122 -12
- package/dist/managers/slashCommandManager.js +9 -28
- package/dist/managers/subagentManager.d.ts +7 -0
- package/dist/managers/subagentManager.js +61 -9
- package/dist/managers/workflowManager.js +6 -0
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/services/MarketplaceService.js +36 -14
- package/dist/services/configurationService.d.ts +34 -2
- package/dist/services/configurationService.js +123 -16
- package/dist/services/initializationService.js +2 -2
- package/dist/services/jsonlHandler.d.ts +14 -0
- package/dist/services/jsonlHandler.js +44 -1
- package/dist/services/memory.d.ts +14 -0
- package/dist/services/memory.js +33 -0
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +289 -0
- package/dist/services/remoteSettingsService.js +4 -4
- package/dist/services/session.js +30 -13
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +10 -0
- package/dist/stdio/index.js +10 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +96 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +222 -0
- package/dist/stdio/stdioAgent.js +341 -0
- package/dist/tools/bashTool.js +2 -0
- package/dist/tools/exitPlanMode.js +10 -2
- package/dist/types/agent.d.ts +8 -0
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +6 -1
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/mcp.d.ts +3 -0
- package/dist/types/messaging.d.ts +1 -8
- package/dist/types/skills.d.ts +11 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/container.d.ts +6 -0
- package/dist/utils/container.js +9 -0
- package/dist/utils/containerSetup.d.ts +11 -1
- package/dist/utils/containerSetup.js +26 -11
- package/dist/utils/fileUtils.d.ts +11 -0
- package/dist/utils/fileUtils.js +37 -0
- package/dist/utils/messageOperations.d.ts +0 -18
- package/dist/utils/messageOperations.js +0 -62
- package/dist/utils/subagentParser.js +9 -2
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +6 -1
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenizer for the structure-aware bash parser.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `bashParser.ts`'s text-based heuristics, this tokenizer understands
|
|
5
|
+
* bash quoting so that structural tokens (;, &&, ||, |, &, ( ), do/done/fi…)
|
|
6
|
+
* are only recognized OUTSIDE quotes/substitutions, and words keep enough
|
|
7
|
+
* internal structure for the semantic layer to tell quoted `"$f"` from bare
|
|
8
|
+
* `$f`.
|
|
9
|
+
*
|
|
10
|
+
* Words are tokenized into parts:
|
|
11
|
+
* - lit literal text (single-quoted or plain)
|
|
12
|
+
* - var `$name` / `${name}` expansion (name kept raw, unexpanded)
|
|
13
|
+
* - special `$@` `$*` `$?` `$$` `$!` `$#` `$-` `$0`..`$9`
|
|
14
|
+
* - cmdsub `$(...)` or backtick command substitution (text = inner)
|
|
15
|
+
* - arith `$((...))` — unsupported (dynamic), fail closed
|
|
16
|
+
* - brace `{a,b}` brace expansion — unsupported (dynamic)
|
|
17
|
+
* - process `<(cmd)` / `>(cmd)` process substitution — unsupported
|
|
18
|
+
* Each part records whether it appeared inside double quotes (`q: 'double'`)
|
|
19
|
+
* or bare (`q: 'none'`); single-quoted text is fully literal.
|
|
20
|
+
*
|
|
21
|
+
* The tokenizer FAILS (returns null) on any construct it cannot safely delimit
|
|
22
|
+
* (unterminated quotes / $( / backticks / heredocs). Failures are surfaced by
|
|
23
|
+
* the caller as a fail-closed `unsupported` parse result.
|
|
24
|
+
*/
|
|
25
|
+
const LENGTH_LIMIT = 10000;
|
|
26
|
+
/** Control operator punctuation (2-char forms first). */
|
|
27
|
+
const OP_2CHAR = ["&&", "||", "|&", ";;", ";&", ";;&"];
|
|
28
|
+
const OP_1CHAR = new Set(["&", "|", ";", "(", ")", "\n"]);
|
|
29
|
+
const NAME_START = /[A-Za-z_]/;
|
|
30
|
+
const NAME_CHAR = /[A-Za-z0-9_]/;
|
|
31
|
+
export function isVariableNameStart(ch) {
|
|
32
|
+
return NAME_START.test(ch);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Classify a redirection operator word emitted by the tokenizer.
|
|
36
|
+
* Returns "plain" when the operator still needs a separate following target
|
|
37
|
+
* word (`>`, `2>`, `>>`, `&>`, `>&` …), "folded" when the target is already
|
|
38
|
+
* part of the operator token (`2>&1`, `>&-`), or null when the word is not a
|
|
39
|
+
* redirection operator.
|
|
40
|
+
*
|
|
41
|
+
* The raw word may carry an optional file-descriptor digit prefix (`2>`) and
|
|
42
|
+
* the dup operators `<&`/`>&` may fold their fd/- target when it directly
|
|
43
|
+
* touches the operator (`2>&1` must stay one token so the reconstructed text
|
|
44
|
+
* remains faithful — legacy `2>& 1` text checks would mis-read the target).
|
|
45
|
+
*/
|
|
46
|
+
export function redirectOpKind(raw) {
|
|
47
|
+
const m = raw.match(/^(\d*)(&>>|&>|<<<|<<-|<<|<>|>>|<&|>&|>|<)(\d*|-)?$/);
|
|
48
|
+
if (!m)
|
|
49
|
+
return null;
|
|
50
|
+
const op = m[2];
|
|
51
|
+
const tail = m[3] ?? "";
|
|
52
|
+
if (op === "<&" || op === ">&") {
|
|
53
|
+
// Folded dup has its fd/- target inside the token; a bare `>&`/`<&` (the
|
|
54
|
+
// target was spaced apart) still needs a following word.
|
|
55
|
+
return tail === "" ? "plain" : "folded";
|
|
56
|
+
}
|
|
57
|
+
// digits/- tail is only legal on dup operators
|
|
58
|
+
if (tail !== "")
|
|
59
|
+
return null;
|
|
60
|
+
return "plain";
|
|
61
|
+
}
|
|
62
|
+
/** True when the raw text is a redirection operator (fd prefix allowed). */
|
|
63
|
+
export function isRedirectRaw(raw) {
|
|
64
|
+
return redirectOpKind(raw) !== null;
|
|
65
|
+
}
|
|
66
|
+
/** Whether a word token is a redirection operator (raw literal, no quoting). */
|
|
67
|
+
export function isRedirectWord(w) {
|
|
68
|
+
const p = w.parts[0];
|
|
69
|
+
// Quoted `">"` / `'2>&1'` are ordinary arguments, not operators.
|
|
70
|
+
if (w.parts.length !== 1 || p?.t !== "lit" || p.q !== "none")
|
|
71
|
+
return false;
|
|
72
|
+
return isRedirectRaw(w.raw);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Tokenize a bash command string. Returns tokens or null on a lexical
|
|
76
|
+
* construct we deliberately do not support (fail closed).
|
|
77
|
+
*/
|
|
78
|
+
export function tokenize(input) {
|
|
79
|
+
if (input.length > LENGTH_LIMIT)
|
|
80
|
+
return null;
|
|
81
|
+
const tokens = [];
|
|
82
|
+
let i = 0;
|
|
83
|
+
const n = input.length;
|
|
84
|
+
const peek = (off = 0) => input[i + off] ?? "";
|
|
85
|
+
const eof = () => i >= n;
|
|
86
|
+
/** Skip whitespace (not newlines — those are structural separators). */
|
|
87
|
+
const skipWs = () => {
|
|
88
|
+
while (!eof() && /[ \t\r]/.test(peek()))
|
|
89
|
+
i++;
|
|
90
|
+
};
|
|
91
|
+
/** Skip a `#` comment to end of line (only when # starts a word). */
|
|
92
|
+
const skipComment = () => {
|
|
93
|
+
while (!eof() && peek() !== "\n")
|
|
94
|
+
i++;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Read the body of a `$(...)` substitution whose opening '(' is at
|
|
98
|
+
* `openParenAt`. Balances nested $(), parens inside quotes are ignored.
|
|
99
|
+
* Returns inner text (exclusive of outer parens) or null if unterminated.
|
|
100
|
+
*/
|
|
101
|
+
const readDollarParen = (openParenAt) => {
|
|
102
|
+
let depth = 1;
|
|
103
|
+
let start = openParenAt + 1;
|
|
104
|
+
let j = openParenAt + 1;
|
|
105
|
+
while (j < n) {
|
|
106
|
+
const c = input[j];
|
|
107
|
+
if (c === "\\") {
|
|
108
|
+
j += 2;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (c === "'") {
|
|
112
|
+
// single quotes: everything literal until next '
|
|
113
|
+
const close = input.indexOf("'", j + 1);
|
|
114
|
+
if (close === -1)
|
|
115
|
+
return null;
|
|
116
|
+
j = close + 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (c === '"') {
|
|
120
|
+
// double quotes may nest $( ) but a ) inside quotes doesn't close us
|
|
121
|
+
const inner = readQuotedDouble(j);
|
|
122
|
+
if (inner === null)
|
|
123
|
+
return null;
|
|
124
|
+
j = inner + 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (c === "`") {
|
|
128
|
+
const end = readBacktick(j);
|
|
129
|
+
if (end === -1)
|
|
130
|
+
return null;
|
|
131
|
+
j = end + 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (c === "$" && input[j + 1] === "(" && input[j + 2] !== "(") {
|
|
135
|
+
depth++;
|
|
136
|
+
j += 2;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (c === "(") {
|
|
140
|
+
// subshell parens inside $() — count them too so a lone ) inside a
|
|
141
|
+
// subshell doesn't terminate us: $( (echo a); echo b )
|
|
142
|
+
depth++;
|
|
143
|
+
j++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (c === ")") {
|
|
147
|
+
depth--;
|
|
148
|
+
if (depth === 0)
|
|
149
|
+
return input.slice(start, j);
|
|
150
|
+
j++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
j++;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
};
|
|
157
|
+
/** Read content of a double-quoted string starting at the opening ".
|
|
158
|
+
* Returns index just past the closing ", or null if unterminated. */
|
|
159
|
+
const readQuotedDouble = (at) => {
|
|
160
|
+
let j = at + 1;
|
|
161
|
+
while (j < n) {
|
|
162
|
+
const c = input[j];
|
|
163
|
+
if (c === "\\") {
|
|
164
|
+
j += 2;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (c === '"')
|
|
168
|
+
return j;
|
|
169
|
+
if (c === "$" && input[j + 1] === "(" && input[j + 2] !== "(") {
|
|
170
|
+
const inner = readDollarParenFrom(j + 1);
|
|
171
|
+
if (inner === null)
|
|
172
|
+
return null;
|
|
173
|
+
j = inner;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (c === "`") {
|
|
177
|
+
const end = readBacktick(j);
|
|
178
|
+
if (end === -1)
|
|
179
|
+
return null;
|
|
180
|
+
j = end + 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
j++;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
};
|
|
187
|
+
/** Variant of readDollarParen used when scanning inside a double-quote:
|
|
188
|
+
* returns index just past the matching ')' of the $() starting at `at`. */
|
|
189
|
+
const readDollarParenFrom = (openParenAt) => {
|
|
190
|
+
let depth = 1;
|
|
191
|
+
let j = openParenAt + 1;
|
|
192
|
+
while (j < n) {
|
|
193
|
+
const c = input[j];
|
|
194
|
+
if (c === "\\") {
|
|
195
|
+
j += 2;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (c === "'") {
|
|
199
|
+
const close = input.indexOf("'", j + 1);
|
|
200
|
+
if (close === -1)
|
|
201
|
+
return null;
|
|
202
|
+
j = close + 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (c === '"') {
|
|
206
|
+
const inner = readQuotedDouble(j);
|
|
207
|
+
if (inner === null)
|
|
208
|
+
return null;
|
|
209
|
+
j = inner + 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (c === "`") {
|
|
213
|
+
const end = readBacktick(j);
|
|
214
|
+
if (end === -1)
|
|
215
|
+
return null;
|
|
216
|
+
j = end + 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (c === "(") {
|
|
220
|
+
depth++;
|
|
221
|
+
j++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (c === ")") {
|
|
225
|
+
depth--;
|
|
226
|
+
if (depth === 0)
|
|
227
|
+
return j + 1;
|
|
228
|
+
j++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
j++;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
};
|
|
235
|
+
/** Read a backtick substitution body. Returns index of closing backtick or -1. */
|
|
236
|
+
const readBacktick = (at) => {
|
|
237
|
+
let j = at + 1;
|
|
238
|
+
while (j < n) {
|
|
239
|
+
const c = input[j];
|
|
240
|
+
if (c === "\\") {
|
|
241
|
+
j += 2;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (c === "`")
|
|
245
|
+
return j;
|
|
246
|
+
j++;
|
|
247
|
+
}
|
|
248
|
+
return -1;
|
|
249
|
+
};
|
|
250
|
+
/** Read a $((...)) arithmetic expansion. Returns end index or -1. */
|
|
251
|
+
const readArith = (at) => {
|
|
252
|
+
// at is at the second '('
|
|
253
|
+
let depth = 1;
|
|
254
|
+
let j = at + 1;
|
|
255
|
+
while (j < n) {
|
|
256
|
+
const c = input[j];
|
|
257
|
+
if (c === "\\") {
|
|
258
|
+
j += 2;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (c === "'") {
|
|
262
|
+
const close = input.indexOf("'", j + 1);
|
|
263
|
+
if (close === -1)
|
|
264
|
+
return -1;
|
|
265
|
+
j = close + 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (c === '"') {
|
|
269
|
+
const inner = readQuotedDouble(j);
|
|
270
|
+
if (inner === null)
|
|
271
|
+
return -1;
|
|
272
|
+
j = inner + 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (c === "$" && input[j + 1] === "(" && input[j + 2] === "(") {
|
|
276
|
+
depth++;
|
|
277
|
+
j += 3;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (c === "(") {
|
|
281
|
+
depth++;
|
|
282
|
+
j++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (c === ")") {
|
|
286
|
+
depth--;
|
|
287
|
+
if (depth === 0) {
|
|
288
|
+
// closing for $(( )) is '))' — the second ) consumed next iteration;
|
|
289
|
+
// simplest: require one more ')'
|
|
290
|
+
return j + 1 <= n ? j + 1 : -1;
|
|
291
|
+
}
|
|
292
|
+
j++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
j++;
|
|
296
|
+
}
|
|
297
|
+
return -1;
|
|
298
|
+
};
|
|
299
|
+
/** Try reading a `${...}` parameter expansion. Returns name | null. */
|
|
300
|
+
const readBraceParam = () => {
|
|
301
|
+
// i at '$', input[i+1] === '{'
|
|
302
|
+
let j = i + 2;
|
|
303
|
+
const start = j;
|
|
304
|
+
while (j < n) {
|
|
305
|
+
const c = input[j];
|
|
306
|
+
if (c === "}") {
|
|
307
|
+
return input.slice(start, j);
|
|
308
|
+
}
|
|
309
|
+
if (c === "{" || c === "(" || c === "$") {
|
|
310
|
+
// nested expansion inside ${...} — too dynamic
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
j++;
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
};
|
|
317
|
+
/** Scan a full word starting at i. Appends a word token; returns new i or -1. */
|
|
318
|
+
const scanWord = () => {
|
|
319
|
+
const rawStart = i;
|
|
320
|
+
const parts = [];
|
|
321
|
+
let litStart = -1; // start index of pending literal text
|
|
322
|
+
const flushLit = (end, q) => {
|
|
323
|
+
if (litStart !== -1) {
|
|
324
|
+
const v = input.slice(litStart, end);
|
|
325
|
+
if (v.length > 0)
|
|
326
|
+
parts.push({ t: "lit", v, q });
|
|
327
|
+
litStart = -1;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
let q = "none";
|
|
331
|
+
while (!eof()) {
|
|
332
|
+
const c = peek();
|
|
333
|
+
// unquoted terminators
|
|
334
|
+
if (q === "none" && (c === " " || c === "\t" || c === "\r"))
|
|
335
|
+
break;
|
|
336
|
+
if (q === "none" && c === "\n")
|
|
337
|
+
break;
|
|
338
|
+
if (q === "none" &&
|
|
339
|
+
(c === "&" ||
|
|
340
|
+
c === "|" ||
|
|
341
|
+
c === ";" ||
|
|
342
|
+
c === "(" ||
|
|
343
|
+
c === ")" ||
|
|
344
|
+
c === "<" ||
|
|
345
|
+
c === ">")) {
|
|
346
|
+
// `<` / `>` outside quotes are redirection operators, never part of a
|
|
347
|
+
// word (bash: `foo>bar` redirects bar into foo).
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
// `#` is only a comment when it starts a word (handled in the main loop
|
|
351
|
+
// after whitespace); inside a word it is a literal character.
|
|
352
|
+
if (q === "single") {
|
|
353
|
+
if (c === "'") {
|
|
354
|
+
flushLit(i, "single");
|
|
355
|
+
q = "none";
|
|
356
|
+
i++;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
i++;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (c === "'" && q === "none") {
|
|
363
|
+
flushLit(i, "none");
|
|
364
|
+
q = "single";
|
|
365
|
+
if (litStart === -1)
|
|
366
|
+
litStart = i + 1;
|
|
367
|
+
i++;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (c === '"') {
|
|
371
|
+
flushLit(i, q === "double" ? "double" : "none");
|
|
372
|
+
if (q === "none") {
|
|
373
|
+
q = "double";
|
|
374
|
+
litStart = i + 1;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// closing quote
|
|
378
|
+
q = "none";
|
|
379
|
+
}
|
|
380
|
+
i++;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (q === "double" && c === "\\") {
|
|
384
|
+
// escaped char inside double quotes: keep both chars literal
|
|
385
|
+
i += 2;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (q === "none" && c === "\\") {
|
|
389
|
+
// escape next char
|
|
390
|
+
if (i + 1 >= n)
|
|
391
|
+
return null;
|
|
392
|
+
i += 2;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (c === "$") {
|
|
396
|
+
const n1 = peek(1);
|
|
397
|
+
if (n1 === "(") {
|
|
398
|
+
const n2 = peek(2);
|
|
399
|
+
if (n2 === "(") {
|
|
400
|
+
// $(( arithmetic
|
|
401
|
+
flushLit(i, q);
|
|
402
|
+
const end = readArith(i + 1);
|
|
403
|
+
if (end === -1)
|
|
404
|
+
return null;
|
|
405
|
+
parts.push({ t: "arith", q: q === "double" ? "double" : "none" });
|
|
406
|
+
i = end;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
// $( command substitution — readDollarParen expects the '(' index.
|
|
410
|
+
flushLit(i, q);
|
|
411
|
+
const openParen = i + 1;
|
|
412
|
+
const inner = readDollarParen(openParen);
|
|
413
|
+
if (inner === null)
|
|
414
|
+
return null;
|
|
415
|
+
parts.push({
|
|
416
|
+
t: "cmdsub",
|
|
417
|
+
text: inner,
|
|
418
|
+
q: q === "double" ? "double" : "none",
|
|
419
|
+
});
|
|
420
|
+
// Skip '$', '(', inner text and the closing ')'.
|
|
421
|
+
i = openParen + inner.length + 2;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (n1 === "{") {
|
|
425
|
+
flushLit(i, q);
|
|
426
|
+
const name = readBraceParam();
|
|
427
|
+
if (name === null)
|
|
428
|
+
return null;
|
|
429
|
+
if (name === "")
|
|
430
|
+
return null;
|
|
431
|
+
if (/^[0-9]+$/.test(name) || name === "@" || name === "*") {
|
|
432
|
+
parts.push({
|
|
433
|
+
t: "special",
|
|
434
|
+
name,
|
|
435
|
+
q: q === "double" ? "double" : "none",
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
else if (isVariableNameStart(name[0])) {
|
|
439
|
+
parts.push({
|
|
440
|
+
t: "var",
|
|
441
|
+
name,
|
|
442
|
+
q: q === "double" ? "double" : "none",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
// ${!x} / ${x:-y} / ${x#...} — dynamic, unsupported
|
|
447
|
+
parts.push({ t: "brace", v: `\${${name}}`, q: "none" });
|
|
448
|
+
}
|
|
449
|
+
i = i + 2 + name.length + 1;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
// bare $name / $@ / $? etc
|
|
453
|
+
const n1Char = n1;
|
|
454
|
+
if (n1Char === "@" || n1Char === "*") {
|
|
455
|
+
flushLit(i, q);
|
|
456
|
+
parts.push({
|
|
457
|
+
t: "special",
|
|
458
|
+
name: n1Char,
|
|
459
|
+
q: q === "double" ? "double" : "none",
|
|
460
|
+
});
|
|
461
|
+
i += 2;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (n1Char === "?" ||
|
|
465
|
+
n1Char === "$" ||
|
|
466
|
+
n1Char === "!" ||
|
|
467
|
+
n1Char === "#" ||
|
|
468
|
+
n1Char === "-" ||
|
|
469
|
+
/[0-9]/.test(n1Char)) {
|
|
470
|
+
flushLit(i, q);
|
|
471
|
+
parts.push({
|
|
472
|
+
t: "special",
|
|
473
|
+
name: n1Char,
|
|
474
|
+
q: q === "double" ? "double" : "none",
|
|
475
|
+
});
|
|
476
|
+
i += 2;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (isVariableNameStart(n1Char)) {
|
|
480
|
+
flushLit(i, q);
|
|
481
|
+
let j = i + 1;
|
|
482
|
+
while (j < n && NAME_CHAR.test(input[j]))
|
|
483
|
+
j++;
|
|
484
|
+
parts.push({
|
|
485
|
+
t: "var",
|
|
486
|
+
name: input.slice(i + 1, j),
|
|
487
|
+
q: q === "double" ? "double" : "none",
|
|
488
|
+
});
|
|
489
|
+
i = j;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
// lone '$' — treat as literal
|
|
493
|
+
i++;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (c === "`") {
|
|
497
|
+
flushLit(i, q);
|
|
498
|
+
const end = readBacktick(i);
|
|
499
|
+
if (end === -1)
|
|
500
|
+
return null;
|
|
501
|
+
parts.push({
|
|
502
|
+
t: "cmdsub",
|
|
503
|
+
text: input.slice(i + 1, end),
|
|
504
|
+
q: q === "double" ? "double" : "none",
|
|
505
|
+
});
|
|
506
|
+
i = end + 1;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
// process substitution <( / >( is handled at operator level (see below)
|
|
510
|
+
if (litStart === -1)
|
|
511
|
+
litStart = i;
|
|
512
|
+
i++;
|
|
513
|
+
}
|
|
514
|
+
flushLit(i, q === "double" ? "double" : "none");
|
|
515
|
+
if (i <= rawStart)
|
|
516
|
+
return -1;
|
|
517
|
+
const raw = input.slice(rawStart, i);
|
|
518
|
+
tokens.push({ t: "word", raw, parts });
|
|
519
|
+
return i;
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* If input[i] begins a redirection operator, return the operator text to
|
|
523
|
+
* emit as a word token (fd prefix + operator, plus a folded dup target like
|
|
524
|
+
* `2>&1`). Digits only form an fd prefix when they directly touch the
|
|
525
|
+
* operator — `2>/dev/null` is one operator, `2 > /dev/null` is the argument
|
|
526
|
+
* `2` followed by a stdout redirect. Returns null when i is not a redirect,
|
|
527
|
+
* or the "<heredoc>" sentinel for `<<`/`<<-`/`<<<` (unmodelled → fail
|
|
528
|
+
* closed).
|
|
529
|
+
*/
|
|
530
|
+
const readRedirectOp = () => {
|
|
531
|
+
const start = i;
|
|
532
|
+
let k = i;
|
|
533
|
+
// optional fd-prefix digits touching the operator
|
|
534
|
+
if (/[0-9]/.test(input[k] ?? "")) {
|
|
535
|
+
while (k < n && /[0-9]/.test(input[k]))
|
|
536
|
+
k++;
|
|
537
|
+
if (input[k] !== "<" && input[k] !== ">")
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
const op = input[k] ?? "";
|
|
541
|
+
if (op === "&") {
|
|
542
|
+
// &> / &>> — redirect both streams to a following target word
|
|
543
|
+
if (input[k + 1] !== ">")
|
|
544
|
+
return null; // plain `&` background operator
|
|
545
|
+
k++;
|
|
546
|
+
if (input[k + 1] === ">")
|
|
547
|
+
k++;
|
|
548
|
+
return input.slice(start, k + 1);
|
|
549
|
+
}
|
|
550
|
+
if (op !== "<" && op !== ">")
|
|
551
|
+
return null;
|
|
552
|
+
const next = input[k + 1] ?? "";
|
|
553
|
+
if (op === "<") {
|
|
554
|
+
if (next === "<")
|
|
555
|
+
return "<heredoc>"; // << <<- <<<
|
|
556
|
+
if (next === "&") {
|
|
557
|
+
// <&fd dup — fold an attached fd / `-` into the token
|
|
558
|
+
k++;
|
|
559
|
+
if (input[k + 1] === "-")
|
|
560
|
+
return input.slice(start, k + 2);
|
|
561
|
+
let j = k + 1;
|
|
562
|
+
while (j < n && /[0-9]/.test(input[j]))
|
|
563
|
+
j++;
|
|
564
|
+
if (j > k + 1)
|
|
565
|
+
return input.slice(start, j);
|
|
566
|
+
return input.slice(start, k + 1);
|
|
567
|
+
}
|
|
568
|
+
if (next === ">")
|
|
569
|
+
return input.slice(start, k + 2); // <>
|
|
570
|
+
return input.slice(start, k + 1); // <
|
|
571
|
+
}
|
|
572
|
+
// '>'
|
|
573
|
+
if (next === ">")
|
|
574
|
+
return input.slice(start, k + 2); // >>
|
|
575
|
+
if (next === "&") {
|
|
576
|
+
// >&fd dup — fold an attached fd / `-` into the token
|
|
577
|
+
k++;
|
|
578
|
+
if (input[k + 1] === "-")
|
|
579
|
+
return input.slice(start, k + 2);
|
|
580
|
+
let j = k + 1;
|
|
581
|
+
while (j < n && /[0-9]/.test(input[j]))
|
|
582
|
+
j++;
|
|
583
|
+
if (j > k + 1)
|
|
584
|
+
return input.slice(start, j);
|
|
585
|
+
return input.slice(start, k + 1);
|
|
586
|
+
}
|
|
587
|
+
return input.slice(start, k + 1); // >
|
|
588
|
+
};
|
|
589
|
+
while (!eof()) {
|
|
590
|
+
skipWs();
|
|
591
|
+
if (eof())
|
|
592
|
+
break;
|
|
593
|
+
const c = peek();
|
|
594
|
+
if (c === "\n") {
|
|
595
|
+
tokens.push({ t: "nl" });
|
|
596
|
+
i++;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (c === "#") {
|
|
600
|
+
skipComment();
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
// process substitution <( ... ) / >( ... )
|
|
604
|
+
if ((c === "<" || c === ">") && peek(1) === "(") {
|
|
605
|
+
// Read the inner command up to matching ')' (a full command list).
|
|
606
|
+
const inner = readDollarParenFrom(i + 1);
|
|
607
|
+
if (inner === null)
|
|
608
|
+
return null;
|
|
609
|
+
const raw = input.slice(i, inner);
|
|
610
|
+
tokens.push({
|
|
611
|
+
t: "word",
|
|
612
|
+
raw,
|
|
613
|
+
parts: [{ t: "process", text: raw, q: "none" }],
|
|
614
|
+
});
|
|
615
|
+
i = inner;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
// redirection operators (optional fd prefix / folded dup targets). The
|
|
619
|
+
// operator is emitted as a plain word; the (unfolded) target is scanned
|
|
620
|
+
// as the next word. Heredoc bodies are unmodelled → fail closed.
|
|
621
|
+
const rdr = readRedirectOp();
|
|
622
|
+
if (rdr !== null) {
|
|
623
|
+
if (rdr === "<heredoc>")
|
|
624
|
+
return null;
|
|
625
|
+
tokens.push({
|
|
626
|
+
t: "word",
|
|
627
|
+
raw: rdr,
|
|
628
|
+
parts: [{ t: "lit", v: rdr, q: "none" }],
|
|
629
|
+
});
|
|
630
|
+
i += rdr.length;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const two = input.slice(i, i + 2);
|
|
634
|
+
if (OP_2CHAR.includes(two)) {
|
|
635
|
+
tokens.push({ t: "op", op: two });
|
|
636
|
+
i += 2;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (OP_1CHAR.has(c)) {
|
|
640
|
+
if (c === "\n") {
|
|
641
|
+
tokens.push({ t: "nl" });
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
tokens.push({ t: "op", op: c });
|
|
645
|
+
}
|
|
646
|
+
i++;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
// `{` / `}` are reserved words (brace group) only when delimited by
|
|
650
|
+
// whitespace or a control operator; `{a,b}` stays a single word.
|
|
651
|
+
if (c === "{" || c === "}") {
|
|
652
|
+
const after = peek(1);
|
|
653
|
+
const delimited = after === "" || /[ \t\r\n;|&()<>]/.test(after);
|
|
654
|
+
if (delimited) {
|
|
655
|
+
tokens.push({
|
|
656
|
+
t: "word",
|
|
657
|
+
raw: c,
|
|
658
|
+
parts: [{ t: "lit", v: c, q: "none" }],
|
|
659
|
+
});
|
|
660
|
+
i++;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const next = scanWord();
|
|
664
|
+
if (next === null)
|
|
665
|
+
return null;
|
|
666
|
+
i = next;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const next = scanWord();
|
|
670
|
+
if (next === null)
|
|
671
|
+
return null;
|
|
672
|
+
i = next;
|
|
673
|
+
}
|
|
674
|
+
tokens.push({ t: "eof" });
|
|
675
|
+
return tokens;
|
|
676
|
+
}
|