command-stream 0.7.1 → 0.8.2

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.
@@ -19,7 +19,7 @@ const TokenType = {
19
19
  REDIRECT_OUT: '>',
20
20
  REDIRECT_APPEND: '>>',
21
21
  REDIRECT_IN: '<',
22
- EOF: 'eof'
22
+ EOF: 'eof',
23
23
  };
24
24
 
25
25
  /**
@@ -28,15 +28,17 @@ const TokenType = {
28
28
  function tokenize(command) {
29
29
  const tokens = [];
30
30
  let i = 0;
31
-
31
+
32
32
  while (i < command.length) {
33
33
  // Skip whitespace
34
34
  while (i < command.length && /\s/.test(command[i])) {
35
35
  i++;
36
36
  }
37
-
38
- if (i >= command.length) break;
39
-
37
+
38
+ if (i >= command.length) {
39
+ break;
40
+ }
41
+
40
42
  // Check for operators
41
43
  if (command[i] === '&' && command[i + 1] === '&') {
42
44
  tokens.push({ type: TokenType.AND, value: '&&' });
@@ -70,18 +72,17 @@ function tokenize(command) {
70
72
  let word = '';
71
73
  let inQuote = false;
72
74
  let quoteChar = '';
73
-
75
+
74
76
  while (i < command.length) {
75
77
  const char = command[i];
76
-
78
+
77
79
  if (!inQuote) {
78
80
  if (char === '"' || char === "'") {
79
81
  inQuote = true;
80
82
  quoteChar = char;
81
83
  word += char;
82
84
  i++;
83
- } else if (/\s/.test(char) ||
84
- '&|;()<>'.includes(char)) {
85
+ } else if (/\s/.test(char) || '&|;()<>'.includes(char)) {
85
86
  break;
86
87
  } else if (char === '\\' && i + 1 < command.length) {
87
88
  // Handle escape sequences
@@ -101,8 +102,11 @@ function tokenize(command) {
101
102
  quoteChar = '';
102
103
  word += char;
103
104
  i++;
104
- } else if (char === '\\' && i + 1 < command.length &&
105
- (command[i + 1] === quoteChar || command[i + 1] === '\\')) {
105
+ } else if (
106
+ char === '\\' &&
107
+ i + 1 < command.length &&
108
+ (command[i + 1] === quoteChar || command[i + 1] === '\\')
109
+ ) {
106
110
  // Handle escaped quotes and backslashes inside quotes
107
111
  word += char;
108
112
  i++;
@@ -116,13 +120,13 @@ function tokenize(command) {
116
120
  }
117
121
  }
118
122
  }
119
-
123
+
120
124
  if (word) {
121
125
  tokens.push({ type: TokenType.WORD, value: word });
122
126
  }
123
127
  }
124
128
  }
125
-
129
+
126
130
  tokens.push({ type: TokenType.EOF, value: '' });
127
131
  return tokens;
128
132
  }
@@ -135,52 +139,56 @@ class ShellParser {
135
139
  this.tokens = tokenize(command);
136
140
  this.pos = 0;
137
141
  }
138
-
142
+
139
143
  current() {
140
144
  return this.tokens[this.pos] || { type: TokenType.EOF, value: '' };
141
145
  }
142
-
146
+
143
147
  peek() {
144
148
  return this.tokens[this.pos + 1] || { type: TokenType.EOF, value: '' };
145
149
  }
146
-
150
+
147
151
  consume() {
148
152
  const token = this.current();
149
153
  this.pos++;
150
154
  return token;
151
155
  }
152
-
156
+
153
157
  /**
154
158
  * Parse the main command sequence
155
159
  */
156
160
  parse() {
157
161
  return this.parseSequence();
158
162
  }
159
-
163
+
160
164
  /**
161
165
  * Parse a sequence of commands connected by &&, ||, ;
162
166
  */
163
167
  parseSequence() {
164
168
  const commands = [];
165
169
  const operators = [];
166
-
170
+
167
171
  // Parse first command
168
172
  let cmd = this.parsePipeline();
169
173
  if (cmd) {
170
174
  commands.push(cmd);
171
175
  }
172
-
176
+
173
177
  // Parse additional commands with operators
174
- while (this.current().type !== TokenType.EOF &&
175
- this.current().type !== TokenType.RPAREN) {
178
+ while (
179
+ this.current().type !== TokenType.EOF &&
180
+ this.current().type !== TokenType.RPAREN
181
+ ) {
176
182
  const op = this.current();
177
-
178
- if (op.type === TokenType.AND ||
179
- op.type === TokenType.OR ||
180
- op.type === TokenType.SEMICOLON) {
183
+
184
+ if (
185
+ op.type === TokenType.AND ||
186
+ op.type === TokenType.OR ||
187
+ op.type === TokenType.SEMICOLON
188
+ ) {
181
189
  operators.push(op.type);
182
190
  this.consume();
183
-
191
+
184
192
  cmd = this.parsePipeline();
185
193
  if (cmd) {
186
194
  commands.push(cmd);
@@ -189,29 +197,29 @@ class ShellParser {
189
197
  break;
190
198
  }
191
199
  }
192
-
200
+
193
201
  if (commands.length === 1 && operators.length === 0) {
194
202
  return commands[0];
195
203
  }
196
-
204
+
197
205
  return {
198
206
  type: 'sequence',
199
207
  commands,
200
- operators
208
+ operators,
201
209
  };
202
210
  }
203
-
211
+
204
212
  /**
205
213
  * Parse a pipeline (commands connected by |)
206
214
  */
207
215
  parsePipeline() {
208
216
  const commands = [];
209
-
217
+
210
218
  let cmd = this.parseCommand();
211
219
  if (cmd) {
212
220
  commands.push(cmd);
213
221
  }
214
-
222
+
215
223
  while (this.current().type === TokenType.PIPE) {
216
224
  this.consume();
217
225
  cmd = this.parseCommand();
@@ -219,17 +227,17 @@ class ShellParser {
219
227
  commands.push(cmd);
220
228
  }
221
229
  }
222
-
230
+
223
231
  if (commands.length === 1) {
224
232
  return commands[0];
225
233
  }
226
-
234
+
227
235
  return {
228
236
  type: 'pipeline',
229
- commands
237
+ commands,
230
238
  };
231
239
  }
232
-
240
+
233
241
  /**
234
242
  * Parse a single command or subshell
235
243
  */
@@ -238,43 +246,45 @@ class ShellParser {
238
246
  if (this.current().type === TokenType.LPAREN) {
239
247
  this.consume(); // consume (
240
248
  const subshell = this.parseSequence();
241
-
249
+
242
250
  if (this.current().type === TokenType.RPAREN) {
243
251
  this.consume(); // consume )
244
252
  }
245
-
253
+
246
254
  return {
247
255
  type: 'subshell',
248
- command: subshell
256
+ command: subshell,
249
257
  };
250
258
  }
251
-
259
+
252
260
  // Parse simple command
253
261
  return this.parseSimpleCommand();
254
262
  }
255
-
263
+
256
264
  /**
257
265
  * Parse a simple command (command + args + redirections)
258
266
  */
259
267
  parseSimpleCommand() {
260
268
  const words = [];
261
269
  const redirects = [];
262
-
270
+
263
271
  while (this.current().type !== TokenType.EOF) {
264
272
  const token = this.current();
265
-
273
+
266
274
  if (token.type === TokenType.WORD) {
267
275
  words.push(token.value);
268
276
  this.consume();
269
- } else if (token.type === TokenType.REDIRECT_OUT ||
270
- token.type === TokenType.REDIRECT_APPEND ||
271
- token.type === TokenType.REDIRECT_IN) {
277
+ } else if (
278
+ token.type === TokenType.REDIRECT_OUT ||
279
+ token.type === TokenType.REDIRECT_APPEND ||
280
+ token.type === TokenType.REDIRECT_IN
281
+ ) {
272
282
  this.consume();
273
283
  const target = this.current();
274
284
  if (target.type === TokenType.WORD) {
275
285
  redirects.push({
276
286
  type: token.type,
277
- target: target.value
287
+ target: target.value,
278
288
  });
279
289
  this.consume();
280
290
  }
@@ -282,38 +292,40 @@ class ShellParser {
282
292
  break;
283
293
  }
284
294
  }
285
-
295
+
286
296
  if (words.length === 0) {
287
297
  return null;
288
298
  }
289
-
299
+
290
300
  const cmd = words[0];
291
- const args = words.slice(1).map(word => {
301
+ const args = words.slice(1).map((word) => {
292
302
  // Remove quotes if present
293
- if ((word.startsWith('"') && word.endsWith('"')) ||
294
- (word.startsWith("'") && word.endsWith("'"))) {
303
+ if (
304
+ (word.startsWith('"') && word.endsWith('"')) ||
305
+ (word.startsWith("'") && word.endsWith("'"))
306
+ ) {
295
307
  return {
296
308
  value: word.slice(1, -1),
297
309
  quoted: true,
298
- quoteChar: word[0]
310
+ quoteChar: word[0],
299
311
  };
300
312
  }
301
313
  return {
302
314
  value: word,
303
- quoted: false
315
+ quoted: false,
304
316
  };
305
317
  });
306
-
318
+
307
319
  const result = {
308
320
  type: 'simple',
309
321
  cmd,
310
- args
322
+ args,
311
323
  };
312
-
324
+
313
325
  if (redirects.length > 0) {
314
326
  result.redirects = redirects;
315
327
  }
316
-
328
+
317
329
  return result;
318
330
  }
319
331
  }
@@ -325,19 +337,35 @@ export function parseShellCommand(command) {
325
337
  try {
326
338
  const parser = new ShellParser(command);
327
339
  const result = parser.parse();
328
-
329
- trace('ShellParser', () => `Parsed command | ${JSON.stringify({
330
- input: command.slice(0, 100),
331
- result
332
- }, null, 2)}`);
333
-
340
+
341
+ trace(
342
+ 'ShellParser',
343
+ () =>
344
+ `Parsed command | ${JSON.stringify(
345
+ {
346
+ input: command.slice(0, 100),
347
+ result,
348
+ },
349
+ null,
350
+ 2
351
+ )}`
352
+ );
353
+
334
354
  return result;
335
355
  } catch (error) {
336
- trace('ShellParser', () => `Parse error | ${JSON.stringify({
337
- command: command.slice(0, 100),
338
- error: error.message
339
- }, null, 2)}`);
340
-
356
+ trace(
357
+ 'ShellParser',
358
+ () =>
359
+ `Parse error | ${JSON.stringify(
360
+ {
361
+ command: command.slice(0, 100),
362
+ error: error.message,
363
+ },
364
+ null,
365
+ 2
366
+ )}`
367
+ );
368
+
341
369
  // Return null to fallback to sh -c
342
370
  return null;
343
371
  }
@@ -349,27 +377,27 @@ export function parseShellCommand(command) {
349
377
  export function needsRealShell(command) {
350
378
  // Check for features we don't handle yet
351
379
  const unsupported = [
352
- '`', // Command substitution
353
- '$(', // Command substitution
354
- '${', // Variable expansion
355
- '~', // Home expansion (at start of word)
356
- '*', // Glob patterns
357
- '?', // Glob patterns
358
- '[', // Glob patterns
359
- '2>', // stderr redirection
360
- '&>', // Combined redirection
361
- '>&', // File descriptor duplication
362
- '<<', // Here documents
363
- '<<<', // Here strings
380
+ '`', // Command substitution
381
+ '$(', // Command substitution
382
+ '${', // Variable expansion
383
+ '~', // Home expansion (at start of word)
384
+ '*', // Glob patterns
385
+ '?', // Glob patterns
386
+ '[', // Glob patterns
387
+ '2>', // stderr redirection
388
+ '&>', // Combined redirection
389
+ '>&', // File descriptor duplication
390
+ '<<', // Here documents
391
+ '<<<', // Here strings
364
392
  ];
365
-
393
+
366
394
  for (const feature of unsupported) {
367
395
  if (command.includes(feature)) {
368
396
  return true;
369
397
  }
370
398
  }
371
-
399
+
372
400
  return false;
373
401
  }
374
402
 
375
- export default { parseShellCommand, needsRealShell };
403
+ export default { parseShellCommand, needsRealShell };