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.
- package/README.md +265 -153
- package/package.json +36 -4
- package/src/$.mjs +3706 -1604
- package/src/$.utils.mjs +14 -6
- package/src/commands/$.basename.mjs +8 -6
- package/src/commands/$.cat.mjs +23 -6
- package/src/commands/$.cd.mjs +13 -4
- package/src/commands/$.cp.mjs +53 -26
- package/src/commands/$.dirname.mjs +6 -4
- package/src/commands/$.echo.mjs +11 -4
- package/src/commands/$.env.mjs +4 -4
- package/src/commands/$.exit.mjs +1 -1
- package/src/commands/$.false.mjs +1 -1
- package/src/commands/$.ls.mjs +29 -16
- package/src/commands/$.mkdir.mjs +25 -9
- package/src/commands/$.mv.mjs +50 -22
- package/src/commands/$.pwd.mjs +7 -4
- package/src/commands/$.rm.mjs +30 -13
- package/src/commands/$.seq.mjs +13 -9
- package/src/commands/$.sleep.mjs +83 -31
- package/src/commands/$.test.mjs +4 -4
- package/src/commands/$.touch.mjs +36 -11
- package/src/commands/$.true.mjs +1 -1
- package/src/commands/$.which.mjs +11 -6
- package/src/commands/$.yes.mjs +67 -23
- package/src/shell-parser.mjs +113 -85
package/src/shell-parser.mjs
CHANGED
|
@@ -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)
|
|
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 (
|
|
105
|
-
|
|
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 (
|
|
175
|
-
|
|
178
|
+
while (
|
|
179
|
+
this.current().type !== TokenType.EOF &&
|
|
180
|
+
this.current().type !== TokenType.RPAREN
|
|
181
|
+
) {
|
|
176
182
|
const op = this.current();
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
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 (
|
|
270
|
-
|
|
271
|
-
|
|
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 (
|
|
294
|
-
|
|
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(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
'`',
|
|
353
|
-
'$(',
|
|
354
|
-
'${',
|
|
355
|
-
'~',
|
|
356
|
-
'*',
|
|
357
|
-
'?',
|
|
358
|
-
'[',
|
|
359
|
-
'2>',
|
|
360
|
-
'&>',
|
|
361
|
-
'>&',
|
|
362
|
-
'<<',
|
|
363
|
-
'<<<',
|
|
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 };
|