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.
Files changed (74) hide show
  1. package/README.md +40 -3
  2. package/dist/cli.js +104 -486
  3. package/dist/commands/doctor.js +427 -0
  4. package/dist/commands/init.js +371 -0
  5. package/dist/constants/api.js +94 -0
  6. package/dist/constants/commands.js +64 -0
  7. package/dist/constants/config.js +56 -0
  8. package/dist/constants/database.js +21 -0
  9. package/dist/constants/errors.js +79 -0
  10. package/dist/constants/index.js +28 -0
  11. package/dist/constants/paths.js +28 -0
  12. package/dist/constants/ui.js +73 -0
  13. package/dist/constants/validation.js +124 -0
  14. package/dist/daemon/lshd.js +11 -32
  15. package/dist/lib/daemon-client-helper.js +7 -4
  16. package/dist/lib/daemon-client.js +9 -2
  17. package/dist/lib/format-utils.js +163 -0
  18. package/dist/lib/fuzzy-match.js +123 -0
  19. package/dist/lib/job-manager.js +2 -1
  20. package/dist/lib/platform-utils.js +211 -0
  21. package/dist/lib/secrets-manager.js +11 -1
  22. package/dist/lib/string-utils.js +128 -0
  23. package/dist/services/daemon/daemon-registrar.js +3 -2
  24. package/dist/services/secrets/secrets.js +119 -59
  25. package/package.json +10 -74
  26. package/dist/app.js +0 -33
  27. package/dist/cicd/analytics.js +0 -261
  28. package/dist/cicd/auth.js +0 -269
  29. package/dist/cicd/cache-manager.js +0 -172
  30. package/dist/cicd/data-retention.js +0 -305
  31. package/dist/cicd/performance-monitor.js +0 -224
  32. package/dist/cicd/webhook-receiver.js +0 -640
  33. package/dist/commands/api.js +0 -346
  34. package/dist/commands/theme.js +0 -261
  35. package/dist/commands/zsh-import.js +0 -240
  36. package/dist/components/App.js +0 -1
  37. package/dist/components/Divider.js +0 -29
  38. package/dist/components/REPL.js +0 -43
  39. package/dist/components/Terminal.js +0 -232
  40. package/dist/components/UserInput.js +0 -30
  41. package/dist/daemon/api-server.js +0 -316
  42. package/dist/daemon/monitoring-api.js +0 -220
  43. package/dist/lib/api-error-handler.js +0 -185
  44. package/dist/lib/associative-arrays.js +0 -285
  45. package/dist/lib/base-api-server.js +0 -290
  46. package/dist/lib/brace-expansion.js +0 -160
  47. package/dist/lib/builtin-commands.js +0 -439
  48. package/dist/lib/executors/builtin-executor.js +0 -52
  49. package/dist/lib/extended-globbing.js +0 -411
  50. package/dist/lib/extended-parameter-expansion.js +0 -227
  51. package/dist/lib/interactive-shell.js +0 -460
  52. package/dist/lib/job-builtins.js +0 -582
  53. package/dist/lib/pathname-expansion.js +0 -216
  54. package/dist/lib/script-runner.js +0 -226
  55. package/dist/lib/shell-executor.js +0 -2504
  56. package/dist/lib/shell-parser.js +0 -958
  57. package/dist/lib/shell-types.js +0 -6
  58. package/dist/lib/shell.lib.js +0 -40
  59. package/dist/lib/theme-manager.js +0 -476
  60. package/dist/lib/variable-expansion.js +0 -385
  61. package/dist/lib/zsh-compatibility.js +0 -659
  62. package/dist/lib/zsh-import-manager.js +0 -707
  63. package/dist/lib/zsh-options.js +0 -328
  64. package/dist/pipeline/job-tracker.js +0 -491
  65. package/dist/pipeline/mcli-bridge.js +0 -309
  66. package/dist/pipeline/pipeline-service.js +0 -1119
  67. package/dist/pipeline/workflow-engine.js +0 -870
  68. package/dist/services/api/api.js +0 -58
  69. package/dist/services/api/auth.js +0 -35
  70. package/dist/services/api/config.js +0 -7
  71. package/dist/services/api/file.js +0 -22
  72. package/dist/services/shell/shell.js +0 -28
  73. package/dist/services/zapier.js +0 -16
  74. package/dist/simple-api-server.js +0 -148
@@ -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
- }