lsh-framework 1.2.0 → 1.3.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/README.md +40 -3
- package/dist/cli.js +104 -486
- package/dist/commands/doctor.js +427 -0
- package/dist/commands/init.js +371 -0
- package/dist/constants/api.js +94 -0
- package/dist/constants/commands.js +64 -0
- package/dist/constants/config.js +56 -0
- package/dist/constants/database.js +21 -0
- package/dist/constants/errors.js +79 -0
- package/dist/constants/index.js +28 -0
- package/dist/constants/paths.js +28 -0
- package/dist/constants/ui.js +73 -0
- package/dist/constants/validation.js +124 -0
- package/dist/daemon/lshd.js +11 -32
- package/dist/lib/daemon-client-helper.js +7 -4
- package/dist/lib/daemon-client.js +9 -2
- package/dist/lib/format-utils.js +163 -0
- package/dist/lib/fuzzy-match.js +123 -0
- package/dist/lib/job-manager.js +2 -1
- package/dist/lib/platform-utils.js +211 -0
- package/dist/lib/secrets-manager.js +11 -1
- package/dist/lib/string-utils.js +128 -0
- package/dist/services/daemon/daemon-registrar.js +3 -2
- package/dist/services/secrets/secrets.js +119 -59
- package/package.json +10 -74
- package/dist/app.js +0 -33
- package/dist/cicd/analytics.js +0 -261
- package/dist/cicd/auth.js +0 -269
- package/dist/cicd/cache-manager.js +0 -172
- package/dist/cicd/data-retention.js +0 -305
- package/dist/cicd/performance-monitor.js +0 -224
- package/dist/cicd/webhook-receiver.js +0 -640
- package/dist/commands/api.js +0 -346
- package/dist/commands/theme.js +0 -261
- package/dist/commands/zsh-import.js +0 -240
- package/dist/components/App.js +0 -1
- package/dist/components/Divider.js +0 -29
- package/dist/components/REPL.js +0 -43
- package/dist/components/Terminal.js +0 -232
- package/dist/components/UserInput.js +0 -30
- package/dist/daemon/api-server.js +0 -316
- package/dist/daemon/monitoring-api.js +0 -220
- package/dist/lib/api-error-handler.js +0 -185
- package/dist/lib/associative-arrays.js +0 -285
- package/dist/lib/base-api-server.js +0 -290
- package/dist/lib/brace-expansion.js +0 -160
- package/dist/lib/builtin-commands.js +0 -439
- package/dist/lib/executors/builtin-executor.js +0 -52
- package/dist/lib/extended-globbing.js +0 -411
- package/dist/lib/extended-parameter-expansion.js +0 -227
- package/dist/lib/interactive-shell.js +0 -460
- package/dist/lib/job-builtins.js +0 -582
- package/dist/lib/pathname-expansion.js +0 -216
- package/dist/lib/script-runner.js +0 -226
- package/dist/lib/shell-executor.js +0 -2504
- package/dist/lib/shell-parser.js +0 -958
- package/dist/lib/shell-types.js +0 -6
- package/dist/lib/shell.lib.js +0 -40
- package/dist/lib/theme-manager.js +0 -476
- package/dist/lib/variable-expansion.js +0 -385
- package/dist/lib/zsh-compatibility.js +0 -659
- package/dist/lib/zsh-import-manager.js +0 -707
- package/dist/lib/zsh-options.js +0 -328
- package/dist/pipeline/job-tracker.js +0 -491
- package/dist/pipeline/mcli-bridge.js +0 -309
- package/dist/pipeline/pipeline-service.js +0 -1119
- package/dist/pipeline/workflow-engine.js +0 -870
- package/dist/services/api/api.js +0 -58
- package/dist/services/api/auth.js +0 -35
- package/dist/services/api/config.js +0 -7
- package/dist/services/api/file.js +0 -22
- package/dist/services/shell/shell.js +0 -28
- package/dist/services/zapier.js +0 -16
- package/dist/simple-api-server.js +0 -148
package/dist/lib/shell-parser.js
DELETED
|
@@ -1,958 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POSIX Shell Grammar Parser
|
|
3
|
-
* Implements lexical analysis and syntax parsing for POSIX shell commands
|
|
4
|
-
*/
|
|
5
|
-
export var TokenType;
|
|
6
|
-
(function (TokenType) {
|
|
7
|
-
// Literals and identifiers
|
|
8
|
-
TokenType["WORD"] = "WORD";
|
|
9
|
-
TokenType["NUMBER"] = "NUMBER";
|
|
10
|
-
// Operators
|
|
11
|
-
TokenType["PIPE"] = "PIPE";
|
|
12
|
-
TokenType["AND_IF"] = "AND_IF";
|
|
13
|
-
TokenType["OR_IF"] = "OR_IF";
|
|
14
|
-
TokenType["SEMICOLON"] = "SEMICOLON";
|
|
15
|
-
TokenType["DSEMI"] = "DSEMI";
|
|
16
|
-
TokenType["AMPERSAND"] = "AMPERSAND";
|
|
17
|
-
// Redirection
|
|
18
|
-
TokenType["GREAT"] = "GREAT";
|
|
19
|
-
TokenType["DGREAT"] = "DGREAT";
|
|
20
|
-
TokenType["LESS"] = "LESS";
|
|
21
|
-
TokenType["DLESS"] = "DLESS";
|
|
22
|
-
TokenType["LESSGREAT"] = "LESSGREAT";
|
|
23
|
-
TokenType["DLESSDASH"] = "DLESSDASH";
|
|
24
|
-
// Process substitution
|
|
25
|
-
TokenType["PROC_SUB_IN"] = "PROC_SUB_IN";
|
|
26
|
-
TokenType["PROC_SUB_OUT"] = "PROC_SUB_OUT";
|
|
27
|
-
// Grouping
|
|
28
|
-
TokenType["LPAREN"] = "LPAREN";
|
|
29
|
-
TokenType["RPAREN"] = "RPAREN";
|
|
30
|
-
TokenType["LBRACE"] = "LBRACE";
|
|
31
|
-
TokenType["RBRACE"] = "RBRACE";
|
|
32
|
-
// Special
|
|
33
|
-
TokenType["NEWLINE"] = "NEWLINE";
|
|
34
|
-
TokenType["EOF"] = "EOF";
|
|
35
|
-
// Quotes
|
|
36
|
-
TokenType["SINGLE_QUOTE"] = "SINGLE_QUOTE";
|
|
37
|
-
TokenType["DOUBLE_QUOTE"] = "DOUBLE_QUOTE";
|
|
38
|
-
TokenType["ANSI_C_QUOTE"] = "ANSI_C_QUOTE";
|
|
39
|
-
TokenType["LOCALE_QUOTE"] = "LOCALE_QUOTE";
|
|
40
|
-
TokenType["BACKSLASH"] = "BACKSLASH";
|
|
41
|
-
// Control structure keywords
|
|
42
|
-
TokenType["IF"] = "IF";
|
|
43
|
-
TokenType["THEN"] = "THEN";
|
|
44
|
-
TokenType["ELSE"] = "ELSE";
|
|
45
|
-
TokenType["ELIF"] = "ELIF";
|
|
46
|
-
TokenType["FI"] = "FI";
|
|
47
|
-
TokenType["FOR"] = "FOR";
|
|
48
|
-
TokenType["IN"] = "IN";
|
|
49
|
-
TokenType["DO"] = "DO";
|
|
50
|
-
TokenType["DONE"] = "DONE";
|
|
51
|
-
TokenType["WHILE"] = "WHILE";
|
|
52
|
-
TokenType["UNTIL"] = "UNTIL";
|
|
53
|
-
TokenType["CASE"] = "CASE";
|
|
54
|
-
TokenType["ESAC"] = "ESAC";
|
|
55
|
-
TokenType["FUNCTION"] = "FUNCTION";
|
|
56
|
-
})(TokenType || (TokenType = {}));
|
|
57
|
-
export class ShellLexer {
|
|
58
|
-
input;
|
|
59
|
-
position = 0;
|
|
60
|
-
line = 1;
|
|
61
|
-
column = 1;
|
|
62
|
-
constructor(input) {
|
|
63
|
-
this.input = input;
|
|
64
|
-
}
|
|
65
|
-
peek(offset = 0) {
|
|
66
|
-
const pos = this.position + offset;
|
|
67
|
-
return pos >= this.input.length ? '\0' : this.input[pos];
|
|
68
|
-
}
|
|
69
|
-
advance() {
|
|
70
|
-
if (this.position >= this.input.length)
|
|
71
|
-
return '\0';
|
|
72
|
-
const char = this.input[this.position];
|
|
73
|
-
this.position++;
|
|
74
|
-
if (char === '\n') {
|
|
75
|
-
this.line++;
|
|
76
|
-
this.column = 1;
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
this.column++;
|
|
80
|
-
}
|
|
81
|
-
return char;
|
|
82
|
-
}
|
|
83
|
-
skipWhitespace() {
|
|
84
|
-
while (this.position < this.input.length) {
|
|
85
|
-
const char = this.peek();
|
|
86
|
-
if (char === ' ' || char === '\t' || char === '\r') {
|
|
87
|
-
this.advance();
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
readWord() {
|
|
95
|
-
let word = '';
|
|
96
|
-
let inBraces = false;
|
|
97
|
-
while (this.position < this.input.length) {
|
|
98
|
-
const char = this.peek();
|
|
99
|
-
if (char === '{') {
|
|
100
|
-
inBraces = true;
|
|
101
|
-
word += this.advance();
|
|
102
|
-
}
|
|
103
|
-
else if (char === '}' && inBraces) {
|
|
104
|
-
inBraces = false;
|
|
105
|
-
word += this.advance();
|
|
106
|
-
}
|
|
107
|
-
else if (inBraces) {
|
|
108
|
-
// Inside braces, allow more characters for parameter expansion
|
|
109
|
-
if (char.match(/[a-zA-Z0-9_\-./~:+=?#%*@!$]/)) {
|
|
110
|
-
word += this.advance();
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
// Word characters (including $ for variables, = for assignments, and glob characters)
|
|
118
|
-
if (char.match(/[a-zA-Z0-9_\-./~$=*?[\]]/)) {
|
|
119
|
-
word += this.advance();
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return word;
|
|
127
|
-
}
|
|
128
|
-
readQuotedString(quote) {
|
|
129
|
-
let result = '';
|
|
130
|
-
this.advance(); // consume opening quote
|
|
131
|
-
while (this.position < this.input.length) {
|
|
132
|
-
const char = this.peek();
|
|
133
|
-
if (char === quote) {
|
|
134
|
-
this.advance(); // consume closing quote
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
else if (char === '\\' && quote === '"') {
|
|
138
|
-
// Handle escape sequences in double quotes
|
|
139
|
-
this.advance(); // consume backslash
|
|
140
|
-
const escaped = this.advance();
|
|
141
|
-
result += this.processEscape(escaped);
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
result += this.advance();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return result;
|
|
148
|
-
}
|
|
149
|
-
processEscape(char) {
|
|
150
|
-
switch (char) {
|
|
151
|
-
case 'n': return '\n';
|
|
152
|
-
case 't': return '\t';
|
|
153
|
-
case 'r': return '\r';
|
|
154
|
-
case 'b': return '\b';
|
|
155
|
-
case 'f': return '\f';
|
|
156
|
-
case 'v': return '\v';
|
|
157
|
-
case '\\': return '\\';
|
|
158
|
-
case '"': return '"';
|
|
159
|
-
case "'": return "'";
|
|
160
|
-
default: return char;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
readAnsiCQuotedString() {
|
|
164
|
-
let result = '';
|
|
165
|
-
this.advance(); // consume opening quote '
|
|
166
|
-
while (this.position < this.input.length) {
|
|
167
|
-
const char = this.peek();
|
|
168
|
-
if (char === "'") {
|
|
169
|
-
this.advance(); // consume closing quote
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
else if (char === '\\') {
|
|
173
|
-
// Handle ANSI-C escape sequences
|
|
174
|
-
this.advance(); // consume backslash
|
|
175
|
-
const escaped = this.advance();
|
|
176
|
-
result += this.processAnsiCEscape(escaped);
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
result += this.advance();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
readLocaleQuotedString() {
|
|
185
|
-
let result = '';
|
|
186
|
-
this.advance(); // consume opening quote "
|
|
187
|
-
while (this.position < this.input.length) {
|
|
188
|
-
const char = this.peek();
|
|
189
|
-
if (char === '"') {
|
|
190
|
-
this.advance(); // consume closing quote
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
else if (char === '\\') {
|
|
194
|
-
// Handle escape sequences like double quotes
|
|
195
|
-
this.advance(); // consume backslash
|
|
196
|
-
const escaped = this.advance();
|
|
197
|
-
result += this.processEscape(escaped);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
result += this.advance();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return result;
|
|
204
|
-
}
|
|
205
|
-
readProcessSubstitution() {
|
|
206
|
-
let result = '';
|
|
207
|
-
let parenCount = 1; // We already consumed the opening (
|
|
208
|
-
while (this.position < this.input.length && parenCount > 0) {
|
|
209
|
-
const char = this.peek();
|
|
210
|
-
if (char === '(') {
|
|
211
|
-
parenCount++;
|
|
212
|
-
}
|
|
213
|
-
else if (char === ')') {
|
|
214
|
-
parenCount--;
|
|
215
|
-
}
|
|
216
|
-
if (parenCount > 0) { // Don't include the closing )
|
|
217
|
-
result += this.advance();
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
this.advance(); // consume closing )
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return result;
|
|
224
|
-
}
|
|
225
|
-
processAnsiCEscape(char) {
|
|
226
|
-
switch (char) {
|
|
227
|
-
case 'a': return '\u0007'; // Alert (bell)
|
|
228
|
-
case 'b': return '\b'; // Backspace
|
|
229
|
-
case 'e': return '\u001B'; // Escape character
|
|
230
|
-
case 'f': return '\f'; // Form feed
|
|
231
|
-
case 'n': return '\n'; // Newline
|
|
232
|
-
case 'r': return '\r'; // Carriage return
|
|
233
|
-
case 't': return '\t'; // Horizontal tab
|
|
234
|
-
case 'v': return '\v'; // Vertical tab
|
|
235
|
-
case '\\': return '\\'; // Backslash
|
|
236
|
-
case "'": return "'"; // Single quote
|
|
237
|
-
case '"': return '"'; // Double quote
|
|
238
|
-
case '?': return '?'; // Question mark
|
|
239
|
-
case '0': return '\0'; // Null character
|
|
240
|
-
// Octal sequences \nnn
|
|
241
|
-
case '1':
|
|
242
|
-
case '2':
|
|
243
|
-
case '3':
|
|
244
|
-
case '4':
|
|
245
|
-
case '5':
|
|
246
|
-
case '6':
|
|
247
|
-
case '7':
|
|
248
|
-
// For simplicity, just return the character for now
|
|
249
|
-
// Full implementation would parse \nnn octal sequences
|
|
250
|
-
return char;
|
|
251
|
-
// Hex sequences \xhh
|
|
252
|
-
case 'x':
|
|
253
|
-
// For simplicity, just return 'x' for now
|
|
254
|
-
// Full implementation would parse \xhh hex sequences
|
|
255
|
-
return 'x';
|
|
256
|
-
default:
|
|
257
|
-
return char;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
createToken(type, value) {
|
|
261
|
-
return {
|
|
262
|
-
type,
|
|
263
|
-
value,
|
|
264
|
-
position: this.position,
|
|
265
|
-
line: this.line,
|
|
266
|
-
column: this.column,
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
tokenize() {
|
|
270
|
-
const tokens = [];
|
|
271
|
-
while (this.position < this.input.length) {
|
|
272
|
-
this.skipWhitespace();
|
|
273
|
-
if (this.position >= this.input.length)
|
|
274
|
-
break;
|
|
275
|
-
const char = this.peek();
|
|
276
|
-
const nextChar = this.peek(1);
|
|
277
|
-
// Two-character operators
|
|
278
|
-
if (char === '&' && nextChar === '&') {
|
|
279
|
-
this.advance();
|
|
280
|
-
this.advance();
|
|
281
|
-
tokens.push(this.createToken(TokenType.AND_IF, '&&'));
|
|
282
|
-
}
|
|
283
|
-
else if (char === '|' && nextChar === '|') {
|
|
284
|
-
this.advance();
|
|
285
|
-
this.advance();
|
|
286
|
-
tokens.push(this.createToken(TokenType.OR_IF, '||'));
|
|
287
|
-
}
|
|
288
|
-
else if (char === '>' && nextChar === '>') {
|
|
289
|
-
this.advance();
|
|
290
|
-
this.advance();
|
|
291
|
-
tokens.push(this.createToken(TokenType.DGREAT, '>>'));
|
|
292
|
-
}
|
|
293
|
-
else if (char === '<' && nextChar === '<') {
|
|
294
|
-
this.advance();
|
|
295
|
-
this.advance();
|
|
296
|
-
if (this.peek() === '-') {
|
|
297
|
-
this.advance();
|
|
298
|
-
tokens.push(this.createToken(TokenType.DLESSDASH, '<<-'));
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
tokens.push(this.createToken(TokenType.DLESS, '<<'));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
else if (char === '<' && nextChar === '>') {
|
|
305
|
-
this.advance();
|
|
306
|
-
this.advance();
|
|
307
|
-
tokens.push(this.createToken(TokenType.LESSGREAT, '<>'));
|
|
308
|
-
}
|
|
309
|
-
// Process substitution
|
|
310
|
-
else if (char === '<' && nextChar === '(') {
|
|
311
|
-
this.advance(); // consume <
|
|
312
|
-
this.advance(); // consume (
|
|
313
|
-
const command = this.readProcessSubstitution();
|
|
314
|
-
tokens.push(this.createToken(TokenType.PROC_SUB_IN, '<(' + command + ')'));
|
|
315
|
-
}
|
|
316
|
-
else if (char === '>' && nextChar === '(') {
|
|
317
|
-
this.advance(); // consume >
|
|
318
|
-
this.advance(); // consume (
|
|
319
|
-
const command = this.readProcessSubstitution();
|
|
320
|
-
tokens.push(this.createToken(TokenType.PROC_SUB_OUT, '>(' + command + ')'));
|
|
321
|
-
}
|
|
322
|
-
// Single-character operators
|
|
323
|
-
else if (char === '|') {
|
|
324
|
-
this.advance();
|
|
325
|
-
tokens.push(this.createToken(TokenType.PIPE, '|'));
|
|
326
|
-
}
|
|
327
|
-
else if (char === '&') {
|
|
328
|
-
this.advance();
|
|
329
|
-
tokens.push(this.createToken(TokenType.AMPERSAND, '&'));
|
|
330
|
-
}
|
|
331
|
-
else if (char === ';') {
|
|
332
|
-
const nextChar = this.peek(1);
|
|
333
|
-
if (nextChar === ';') {
|
|
334
|
-
this.advance();
|
|
335
|
-
this.advance();
|
|
336
|
-
tokens.push(this.createToken(TokenType.DSEMI, ';;'));
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
this.advance();
|
|
340
|
-
tokens.push(this.createToken(TokenType.SEMICOLON, ';'));
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
else if (char === '>') {
|
|
344
|
-
this.advance();
|
|
345
|
-
tokens.push(this.createToken(TokenType.GREAT, '>'));
|
|
346
|
-
}
|
|
347
|
-
else if (char === '<') {
|
|
348
|
-
this.advance();
|
|
349
|
-
tokens.push(this.createToken(TokenType.LESS, '<'));
|
|
350
|
-
}
|
|
351
|
-
else if (char === '(') {
|
|
352
|
-
this.advance();
|
|
353
|
-
tokens.push(this.createToken(TokenType.LPAREN, '('));
|
|
354
|
-
}
|
|
355
|
-
else if (char === ')') {
|
|
356
|
-
this.advance();
|
|
357
|
-
tokens.push(this.createToken(TokenType.RPAREN, ')'));
|
|
358
|
-
}
|
|
359
|
-
else if (char === '{') {
|
|
360
|
-
this.advance();
|
|
361
|
-
tokens.push(this.createToken(TokenType.LBRACE, '{'));
|
|
362
|
-
}
|
|
363
|
-
else if (char === '}') {
|
|
364
|
-
this.advance();
|
|
365
|
-
tokens.push(this.createToken(TokenType.RBRACE, '}'));
|
|
366
|
-
}
|
|
367
|
-
else if (char === '\n') {
|
|
368
|
-
this.advance();
|
|
369
|
-
tokens.push(this.createToken(TokenType.NEWLINE, '\n'));
|
|
370
|
-
}
|
|
371
|
-
// Quoted strings
|
|
372
|
-
else if (char === '$' && (this.peek(1) === "'" || this.peek(1) === '"')) {
|
|
373
|
-
// Handle ANSI-C quoting $'...' and locale quoting $"..."
|
|
374
|
-
this.advance(); // consume $
|
|
375
|
-
const quoteChar = this.peek();
|
|
376
|
-
if (quoteChar === "'") {
|
|
377
|
-
const value = this.readAnsiCQuotedString();
|
|
378
|
-
tokens.push(this.createToken(TokenType.ANSI_C_QUOTE, value));
|
|
379
|
-
}
|
|
380
|
-
else if (quoteChar === '"') {
|
|
381
|
-
const value = this.readLocaleQuotedString();
|
|
382
|
-
tokens.push(this.createToken(TokenType.LOCALE_QUOTE, value));
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
else if (char === '"') {
|
|
386
|
-
const value = this.readQuotedString('"');
|
|
387
|
-
tokens.push(this.createToken(TokenType.DOUBLE_QUOTE, value));
|
|
388
|
-
}
|
|
389
|
-
else if (char === "'") {
|
|
390
|
-
const value = this.readQuotedString("'");
|
|
391
|
-
tokens.push(this.createToken(TokenType.SINGLE_QUOTE, value));
|
|
392
|
-
}
|
|
393
|
-
// Words and numbers
|
|
394
|
-
else if (char.match(/[a-zA-Z_$*?[\].]/)) {
|
|
395
|
-
const word = this.readWord();
|
|
396
|
-
const tokenType = this.getKeywordType(word);
|
|
397
|
-
tokens.push(this.createToken(tokenType, word));
|
|
398
|
-
}
|
|
399
|
-
else if (char.match(/[0-9]/)) {
|
|
400
|
-
const number = this.readWord(); // Numbers can contain dots, etc.
|
|
401
|
-
tokens.push(this.createToken(TokenType.NUMBER, number));
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
// Unknown character, treat as word
|
|
405
|
-
const word = this.readWord() || this.advance();
|
|
406
|
-
const tokenType = this.getKeywordType(word);
|
|
407
|
-
tokens.push(this.createToken(tokenType, word));
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
tokens.push(this.createToken(TokenType.EOF, ''));
|
|
411
|
-
return tokens;
|
|
412
|
-
}
|
|
413
|
-
getKeywordType(word) {
|
|
414
|
-
const keywords = {
|
|
415
|
-
'if': TokenType.IF,
|
|
416
|
-
'then': TokenType.THEN,
|
|
417
|
-
'else': TokenType.ELSE,
|
|
418
|
-
'elif': TokenType.ELIF,
|
|
419
|
-
'fi': TokenType.FI,
|
|
420
|
-
'for': TokenType.FOR,
|
|
421
|
-
'in': TokenType.IN,
|
|
422
|
-
'do': TokenType.DO,
|
|
423
|
-
'done': TokenType.DONE,
|
|
424
|
-
'while': TokenType.WHILE,
|
|
425
|
-
'until': TokenType.UNTIL,
|
|
426
|
-
'case': TokenType.CASE,
|
|
427
|
-
'esac': TokenType.ESAC,
|
|
428
|
-
'function': TokenType.FUNCTION,
|
|
429
|
-
};
|
|
430
|
-
return keywords[word] || TokenType.WORD;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
export class ShellParser {
|
|
434
|
-
tokens;
|
|
435
|
-
position = 0;
|
|
436
|
-
constructor(tokens) {
|
|
437
|
-
this.tokens = tokens;
|
|
438
|
-
}
|
|
439
|
-
peek(offset = 0) {
|
|
440
|
-
const pos = this.position + offset;
|
|
441
|
-
return pos >= this.tokens.length
|
|
442
|
-
? { type: TokenType.EOF, value: '', position: 0, line: 0, column: 0 }
|
|
443
|
-
: this.tokens[pos];
|
|
444
|
-
}
|
|
445
|
-
advance() {
|
|
446
|
-
return this.position < this.tokens.length ? this.tokens[this.position++] : this.peek();
|
|
447
|
-
}
|
|
448
|
-
expect(type) {
|
|
449
|
-
const token = this.advance();
|
|
450
|
-
if (token.type !== type) {
|
|
451
|
-
throw new Error(`Expected ${type}, got ${token.type} at line ${token.line}`);
|
|
452
|
-
}
|
|
453
|
-
return token;
|
|
454
|
-
}
|
|
455
|
-
parseSimpleCommand() {
|
|
456
|
-
const name = this.expect(TokenType.WORD).value;
|
|
457
|
-
const args = [];
|
|
458
|
-
const redirections = [];
|
|
459
|
-
while (this.peek().type === TokenType.WORD ||
|
|
460
|
-
this.peek().type === TokenType.NUMBER ||
|
|
461
|
-
this.peek().type === TokenType.SINGLE_QUOTE ||
|
|
462
|
-
this.peek().type === TokenType.DOUBLE_QUOTE ||
|
|
463
|
-
this.peek().type === TokenType.ANSI_C_QUOTE ||
|
|
464
|
-
this.peek().type === TokenType.LOCALE_QUOTE ||
|
|
465
|
-
this.peek().type === TokenType.LBRACE ||
|
|
466
|
-
this.peek().type === TokenType.PROC_SUB_IN ||
|
|
467
|
-
this.peek().type === TokenType.PROC_SUB_OUT ||
|
|
468
|
-
this.isRedirection(this.peek().type)) {
|
|
469
|
-
const token = this.peek();
|
|
470
|
-
if (this.isRedirection(token.type)) {
|
|
471
|
-
redirections.push(this.parseRedirection());
|
|
472
|
-
}
|
|
473
|
-
else if (token.type === TokenType.LBRACE) {
|
|
474
|
-
// Parse brace expression as a single argument
|
|
475
|
-
args.push(this.parseBraceExpression());
|
|
476
|
-
}
|
|
477
|
-
else if (token.type === TokenType.PROC_SUB_IN || token.type === TokenType.PROC_SUB_OUT) {
|
|
478
|
-
// Process substitution will be handled during execution - just store token
|
|
479
|
-
args.push(this.advance().value);
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
args.push(this.advance().value);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return { type: 'SimpleCommand', name, args, redirections };
|
|
486
|
-
}
|
|
487
|
-
parseBraceExpression() {
|
|
488
|
-
let braceExpression = '';
|
|
489
|
-
// Consume opening brace
|
|
490
|
-
braceExpression += this.expect(TokenType.LBRACE).value;
|
|
491
|
-
// Parse content until closing brace
|
|
492
|
-
while (this.peek().type !== TokenType.RBRACE && this.peek().type !== TokenType.EOF) {
|
|
493
|
-
braceExpression += this.advance().value;
|
|
494
|
-
}
|
|
495
|
-
// Consume closing brace
|
|
496
|
-
if (this.peek().type === TokenType.RBRACE) {
|
|
497
|
-
braceExpression += this.advance().value;
|
|
498
|
-
}
|
|
499
|
-
return braceExpression;
|
|
500
|
-
}
|
|
501
|
-
isRedirection(type) {
|
|
502
|
-
return type === TokenType.GREAT || type === TokenType.DGREAT ||
|
|
503
|
-
type === TokenType.LESS || type === TokenType.DLESS ||
|
|
504
|
-
type === TokenType.LESSGREAT || type === TokenType.DLESSDASH;
|
|
505
|
-
}
|
|
506
|
-
parseRedirection() {
|
|
507
|
-
const token = this.advance();
|
|
508
|
-
const target = this.expect(TokenType.WORD).value;
|
|
509
|
-
switch (token.type) {
|
|
510
|
-
case TokenType.GREAT:
|
|
511
|
-
return { type: 'output', target };
|
|
512
|
-
case TokenType.DGREAT:
|
|
513
|
-
return { type: 'append', target };
|
|
514
|
-
case TokenType.LESS:
|
|
515
|
-
return { type: 'input', target };
|
|
516
|
-
case TokenType.DLESS:
|
|
517
|
-
case TokenType.DLESSDASH:
|
|
518
|
-
return { type: 'heredoc', target };
|
|
519
|
-
default:
|
|
520
|
-
throw new Error(`Unknown redirection type: ${token.type}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
parsePipelineElement() {
|
|
524
|
-
const token = this.peek();
|
|
525
|
-
switch (token.type) {
|
|
526
|
-
case TokenType.LPAREN:
|
|
527
|
-
return this.parseSubshell();
|
|
528
|
-
case TokenType.LBRACE:
|
|
529
|
-
return this.parseCommandGroup();
|
|
530
|
-
case TokenType.IF:
|
|
531
|
-
return this.parseIfStatement();
|
|
532
|
-
case TokenType.FOR:
|
|
533
|
-
return this.parseForStatement();
|
|
534
|
-
case TokenType.WHILE:
|
|
535
|
-
return this.parseWhileStatement();
|
|
536
|
-
case TokenType.CASE:
|
|
537
|
-
return this.parseCaseStatement();
|
|
538
|
-
case TokenType.FUNCTION:
|
|
539
|
-
return this.parseFunctionDefinition();
|
|
540
|
-
default:
|
|
541
|
-
// Check if this might be a function definition (name followed by ())
|
|
542
|
-
if (token.type === TokenType.WORD) {
|
|
543
|
-
const nextToken = this.peek(1);
|
|
544
|
-
if (nextToken.type === TokenType.LPAREN) {
|
|
545
|
-
const afterParen = this.peek(2);
|
|
546
|
-
if (afterParen.type === TokenType.RPAREN) {
|
|
547
|
-
return this.parseFunctionDefinition();
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
return this.parseSimpleCommand();
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
parsePipeline() {
|
|
555
|
-
const left = this.parsePipelineElement();
|
|
556
|
-
if (this.peek().type === TokenType.PIPE) {
|
|
557
|
-
const commands = [left];
|
|
558
|
-
while (this.peek().type === TokenType.PIPE) {
|
|
559
|
-
this.advance(); // consume pipe
|
|
560
|
-
commands.push(this.parsePipelineElement());
|
|
561
|
-
}
|
|
562
|
-
return { type: 'Pipeline', commands };
|
|
563
|
-
}
|
|
564
|
-
return left;
|
|
565
|
-
}
|
|
566
|
-
parseCommandList() {
|
|
567
|
-
let left = this.parsePipeline();
|
|
568
|
-
while (this.peek().type === TokenType.AND_IF ||
|
|
569
|
-
this.peek().type === TokenType.OR_IF ||
|
|
570
|
-
this.peek().type === TokenType.SEMICOLON ||
|
|
571
|
-
this.peek().type === TokenType.AMPERSAND) {
|
|
572
|
-
const operator = this.advance();
|
|
573
|
-
let op;
|
|
574
|
-
switch (operator.type) {
|
|
575
|
-
case TokenType.AND_IF:
|
|
576
|
-
op = '&&';
|
|
577
|
-
break;
|
|
578
|
-
case TokenType.OR_IF:
|
|
579
|
-
op = '||';
|
|
580
|
-
break;
|
|
581
|
-
case TokenType.SEMICOLON:
|
|
582
|
-
op = ';';
|
|
583
|
-
break;
|
|
584
|
-
case TokenType.AMPERSAND:
|
|
585
|
-
op = '&';
|
|
586
|
-
break;
|
|
587
|
-
default: throw new Error(`Unknown operator: ${operator.type}`);
|
|
588
|
-
}
|
|
589
|
-
// For background (&), right side is optional (can be at end of line)
|
|
590
|
-
let right;
|
|
591
|
-
if (op === '&' && (this.peek().type === TokenType.EOF || this.peek().type === TokenType.NEWLINE)) {
|
|
592
|
-
right = undefined;
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
right = this.parsePipeline();
|
|
596
|
-
}
|
|
597
|
-
left = { type: 'CommandList', left, operator: op, right };
|
|
598
|
-
}
|
|
599
|
-
return left;
|
|
600
|
-
}
|
|
601
|
-
parseIfStatement() {
|
|
602
|
-
this.expect(TokenType.IF);
|
|
603
|
-
// Skip newlines
|
|
604
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
605
|
-
this.advance();
|
|
606
|
-
}
|
|
607
|
-
const condition = this.parseConditionalCommand();
|
|
608
|
-
// Skip newlines
|
|
609
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
610
|
-
this.advance();
|
|
611
|
-
}
|
|
612
|
-
this.expect(TokenType.THEN);
|
|
613
|
-
// Skip newlines
|
|
614
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
615
|
-
this.advance();
|
|
616
|
-
}
|
|
617
|
-
const thenClause = this.parseCompoundList();
|
|
618
|
-
let elseClause;
|
|
619
|
-
// Skip newlines
|
|
620
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
621
|
-
this.advance();
|
|
622
|
-
}
|
|
623
|
-
if (this.peek().type === TokenType.ELSE) {
|
|
624
|
-
this.advance();
|
|
625
|
-
// Skip newlines
|
|
626
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
627
|
-
this.advance();
|
|
628
|
-
}
|
|
629
|
-
elseClause = this.parseCompoundList();
|
|
630
|
-
// Skip newlines
|
|
631
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
632
|
-
this.advance();
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
this.expect(TokenType.FI);
|
|
636
|
-
return {
|
|
637
|
-
type: 'IfStatement',
|
|
638
|
-
condition,
|
|
639
|
-
thenClause,
|
|
640
|
-
elseClause,
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
parseForStatement() {
|
|
644
|
-
this.expect(TokenType.FOR);
|
|
645
|
-
const variable = this.expect(TokenType.WORD).value;
|
|
646
|
-
// Skip newlines
|
|
647
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
648
|
-
this.advance();
|
|
649
|
-
}
|
|
650
|
-
const words = [];
|
|
651
|
-
if (this.peek().type === TokenType.IN) {
|
|
652
|
-
this.advance();
|
|
653
|
-
// Read word list (including numbers which are treated as words in for loops)
|
|
654
|
-
while (this.peek().type === TokenType.WORD || this.peek().type === TokenType.NUMBER || this.peek().type === TokenType.SINGLE_QUOTE || this.peek().type === TokenType.DOUBLE_QUOTE || this.peek().type === TokenType.ANSI_C_QUOTE || this.peek().type === TokenType.LOCALE_QUOTE) {
|
|
655
|
-
words.push(this.advance().value);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
// Skip newlines and optional semicolon
|
|
659
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
660
|
-
this.advance();
|
|
661
|
-
}
|
|
662
|
-
// Optional semicolon before DO
|
|
663
|
-
if (this.peek().type === TokenType.SEMICOLON) {
|
|
664
|
-
this.advance();
|
|
665
|
-
}
|
|
666
|
-
// Skip newlines after semicolon
|
|
667
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
668
|
-
this.advance();
|
|
669
|
-
}
|
|
670
|
-
this.expect(TokenType.DO);
|
|
671
|
-
// Skip newlines
|
|
672
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
673
|
-
this.advance();
|
|
674
|
-
}
|
|
675
|
-
const body = this.parseCompoundList();
|
|
676
|
-
// Skip newlines
|
|
677
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
678
|
-
this.advance();
|
|
679
|
-
}
|
|
680
|
-
this.expect(TokenType.DONE);
|
|
681
|
-
return {
|
|
682
|
-
type: 'ForStatement',
|
|
683
|
-
variable,
|
|
684
|
-
words,
|
|
685
|
-
body,
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
parseWhileStatement() {
|
|
689
|
-
this.expect(TokenType.WHILE);
|
|
690
|
-
// Skip newlines
|
|
691
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
692
|
-
this.advance();
|
|
693
|
-
}
|
|
694
|
-
const condition = this.parseConditionalCommand();
|
|
695
|
-
// Skip newlines
|
|
696
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
697
|
-
this.advance();
|
|
698
|
-
}
|
|
699
|
-
this.expect(TokenType.DO);
|
|
700
|
-
// Skip newlines
|
|
701
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
702
|
-
this.advance();
|
|
703
|
-
}
|
|
704
|
-
const body = this.parseCompoundList();
|
|
705
|
-
// Skip newlines
|
|
706
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
707
|
-
this.advance();
|
|
708
|
-
}
|
|
709
|
-
this.expect(TokenType.DONE);
|
|
710
|
-
return {
|
|
711
|
-
type: 'WhileStatement',
|
|
712
|
-
condition,
|
|
713
|
-
body,
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
parseCaseStatement() {
|
|
717
|
-
this.expect(TokenType.CASE);
|
|
718
|
-
const word = this.expect(TokenType.WORD).value;
|
|
719
|
-
// Skip newlines
|
|
720
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
721
|
-
this.advance();
|
|
722
|
-
}
|
|
723
|
-
this.expect(TokenType.IN);
|
|
724
|
-
// Skip newlines
|
|
725
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
726
|
-
this.advance();
|
|
727
|
-
}
|
|
728
|
-
const items = [];
|
|
729
|
-
while (this.peek().type !== TokenType.ESAC && this.peek().type !== TokenType.EOF) {
|
|
730
|
-
const patterns = [];
|
|
731
|
-
// Parse patterns separated by |
|
|
732
|
-
patterns.push(this.expect(TokenType.WORD).value);
|
|
733
|
-
while (this.peek().type === TokenType.PIPE) {
|
|
734
|
-
this.advance(); // consume |
|
|
735
|
-
patterns.push(this.expect(TokenType.WORD).value);
|
|
736
|
-
}
|
|
737
|
-
this.advance(); // expect ')' - would need to add to lexer
|
|
738
|
-
// Skip newlines
|
|
739
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
740
|
-
this.advance();
|
|
741
|
-
}
|
|
742
|
-
let command;
|
|
743
|
-
if (this.peek().type !== TokenType.ESAC) {
|
|
744
|
-
command = this.parseCommandList();
|
|
745
|
-
}
|
|
746
|
-
items.push({ patterns, command });
|
|
747
|
-
// Skip newlines
|
|
748
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
749
|
-
this.advance();
|
|
750
|
-
}
|
|
751
|
-
// Skip ;; if present
|
|
752
|
-
if (this.peek().type === TokenType.DSEMI) {
|
|
753
|
-
this.advance();
|
|
754
|
-
}
|
|
755
|
-
// Skip newlines after ;;
|
|
756
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
757
|
-
this.advance();
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
this.expect(TokenType.ESAC);
|
|
761
|
-
return {
|
|
762
|
-
type: 'CaseStatement',
|
|
763
|
-
word,
|
|
764
|
-
items,
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
parseCompoundList() {
|
|
768
|
-
// Parse a sequence of commands until we hit a closing keyword
|
|
769
|
-
const commands = [];
|
|
770
|
-
while (this.peek().type !== TokenType.FI &&
|
|
771
|
-
this.peek().type !== TokenType.DONE &&
|
|
772
|
-
this.peek().type !== TokenType.ESAC &&
|
|
773
|
-
this.peek().type !== TokenType.ELSE &&
|
|
774
|
-
this.peek().type !== TokenType.RBRACE &&
|
|
775
|
-
this.peek().type !== TokenType.EOF) {
|
|
776
|
-
// Skip newlines
|
|
777
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
778
|
-
this.advance();
|
|
779
|
-
}
|
|
780
|
-
if (this.peek().type === TokenType.FI ||
|
|
781
|
-
this.peek().type === TokenType.DONE ||
|
|
782
|
-
this.peek().type === TokenType.ESAC ||
|
|
783
|
-
this.peek().type === TokenType.ELSE ||
|
|
784
|
-
this.peek().type === TokenType.RBRACE ||
|
|
785
|
-
this.peek().type === TokenType.EOF) {
|
|
786
|
-
break;
|
|
787
|
-
}
|
|
788
|
-
// Parse individual commands or pipelines
|
|
789
|
-
commands.push(this.parsePipeline());
|
|
790
|
-
// Skip optional semicolon or newline
|
|
791
|
-
if (this.peek().type === TokenType.SEMICOLON) {
|
|
792
|
-
this.advance();
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
if (commands.length === 0) {
|
|
796
|
-
return { type: 'SimpleCommand', name: '', args: [], redirections: [] };
|
|
797
|
-
}
|
|
798
|
-
else if (commands.length === 1) {
|
|
799
|
-
return commands[0];
|
|
800
|
-
}
|
|
801
|
-
else {
|
|
802
|
-
// Create a command list
|
|
803
|
-
let result = commands[0];
|
|
804
|
-
for (let i = 1; i < commands.length; i++) {
|
|
805
|
-
result = {
|
|
806
|
-
type: 'CommandList',
|
|
807
|
-
left: result,
|
|
808
|
-
operator: ';',
|
|
809
|
-
right: commands[i],
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
return result;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
parseConditionalCommand() {
|
|
816
|
-
// Parse a command list until we hit a control structure keyword
|
|
817
|
-
const commands = [];
|
|
818
|
-
while (this.peek().type !== TokenType.THEN &&
|
|
819
|
-
this.peek().type !== TokenType.DO &&
|
|
820
|
-
this.peek().type !== TokenType.NEWLINE &&
|
|
821
|
-
this.peek().type !== TokenType.EOF) {
|
|
822
|
-
commands.push(this.parsePipeline());
|
|
823
|
-
// Check for command separators
|
|
824
|
-
if (this.peek().type === TokenType.SEMICOLON) {
|
|
825
|
-
this.advance();
|
|
826
|
-
}
|
|
827
|
-
else if (this.peek().type === TokenType.AND_IF || this.peek().type === TokenType.OR_IF) {
|
|
828
|
-
const operator = this.advance();
|
|
829
|
-
const right = this.parsePipeline();
|
|
830
|
-
const op = operator.type === TokenType.AND_IF ? '&&' : '||';
|
|
831
|
-
const lastCmd = commands.pop();
|
|
832
|
-
commands.push({ type: 'CommandList', left: lastCmd, operator: op, right });
|
|
833
|
-
}
|
|
834
|
-
else {
|
|
835
|
-
break;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
if (commands.length === 0) {
|
|
839
|
-
throw new Error('Expected command in conditional');
|
|
840
|
-
}
|
|
841
|
-
else if (commands.length === 1) {
|
|
842
|
-
return commands[0];
|
|
843
|
-
}
|
|
844
|
-
else {
|
|
845
|
-
// Create a command list
|
|
846
|
-
let result = commands[0];
|
|
847
|
-
for (let i = 1; i < commands.length; i++) {
|
|
848
|
-
result = {
|
|
849
|
-
type: 'CommandList',
|
|
850
|
-
left: result,
|
|
851
|
-
operator: ';',
|
|
852
|
-
right: commands[i],
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
return result;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
parseFunctionDefinition() {
|
|
859
|
-
let name;
|
|
860
|
-
// Handle both syntaxes: "function name { ... }" and "name() { ... }"
|
|
861
|
-
if (this.peek().type === TokenType.FUNCTION) {
|
|
862
|
-
this.advance(); // consume 'function'
|
|
863
|
-
name = this.expect(TokenType.WORD).value;
|
|
864
|
-
}
|
|
865
|
-
else {
|
|
866
|
-
// name() syntax
|
|
867
|
-
name = this.expect(TokenType.WORD).value;
|
|
868
|
-
this.expect(TokenType.LPAREN);
|
|
869
|
-
this.expect(TokenType.RPAREN);
|
|
870
|
-
}
|
|
871
|
-
// Skip newlines before body
|
|
872
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
873
|
-
this.advance();
|
|
874
|
-
}
|
|
875
|
-
// Parse function body - expect { ... }
|
|
876
|
-
this.expect(TokenType.LBRACE);
|
|
877
|
-
// Skip newlines after opening brace
|
|
878
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
879
|
-
this.advance();
|
|
880
|
-
}
|
|
881
|
-
// Parse the body as a compound list
|
|
882
|
-
const body = this.parseCompoundList();
|
|
883
|
-
// Skip newlines before closing brace
|
|
884
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
885
|
-
this.advance();
|
|
886
|
-
}
|
|
887
|
-
this.expect(TokenType.RBRACE);
|
|
888
|
-
return {
|
|
889
|
-
type: 'FunctionDefinition',
|
|
890
|
-
name,
|
|
891
|
-
body,
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
parseSubshell() {
|
|
895
|
-
this.expect(TokenType.LPAREN);
|
|
896
|
-
// Skip newlines after opening paren
|
|
897
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
898
|
-
this.advance();
|
|
899
|
-
}
|
|
900
|
-
const command = this.parseCommandList();
|
|
901
|
-
// Skip newlines before closing paren
|
|
902
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
903
|
-
this.advance();
|
|
904
|
-
}
|
|
905
|
-
this.expect(TokenType.RPAREN);
|
|
906
|
-
// Parse redirections after subshell
|
|
907
|
-
const redirections = [];
|
|
908
|
-
while (this.isRedirection(this.peek().type)) {
|
|
909
|
-
redirections.push(this.parseRedirection());
|
|
910
|
-
}
|
|
911
|
-
return {
|
|
912
|
-
type: 'Subshell',
|
|
913
|
-
command,
|
|
914
|
-
redirections,
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
parseCommandGroup() {
|
|
918
|
-
this.expect(TokenType.LBRACE);
|
|
919
|
-
// Skip newlines after opening brace
|
|
920
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
921
|
-
this.advance();
|
|
922
|
-
}
|
|
923
|
-
const command = this.parseCompoundList();
|
|
924
|
-
// Skip newlines before closing brace
|
|
925
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
926
|
-
this.advance();
|
|
927
|
-
}
|
|
928
|
-
this.expect(TokenType.RBRACE);
|
|
929
|
-
// Parse redirections after command group
|
|
930
|
-
const redirections = [];
|
|
931
|
-
while (this.isRedirection(this.peek().type)) {
|
|
932
|
-
redirections.push(this.parseRedirection());
|
|
933
|
-
}
|
|
934
|
-
return {
|
|
935
|
-
type: 'CommandGroup',
|
|
936
|
-
command,
|
|
937
|
-
redirections,
|
|
938
|
-
};
|
|
939
|
-
}
|
|
940
|
-
parse() {
|
|
941
|
-
// Skip leading newlines
|
|
942
|
-
while (this.peek().type === TokenType.NEWLINE) {
|
|
943
|
-
this.advance();
|
|
944
|
-
}
|
|
945
|
-
if (this.peek().type === TokenType.EOF) {
|
|
946
|
-
return { type: 'SimpleCommand', name: '', args: [], redirections: [] };
|
|
947
|
-
}
|
|
948
|
-
// Always use parseCommandList to handle all commands and operators
|
|
949
|
-
return this.parseCommandList();
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
// Convenience function for parsing shell commands
|
|
953
|
-
export function parseShellCommand(input) {
|
|
954
|
-
const lexer = new ShellLexer(input);
|
|
955
|
-
const tokens = lexer.tokenize();
|
|
956
|
-
const parser = new ShellParser(tokens);
|
|
957
|
-
return parser.parse();
|
|
958
|
-
}
|