command-stream 0.5.0 → 0.7.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 +64 -9
- package/package.json +1 -1
- package/src/$.mjs +578 -44
- package/src/commands/$.cd.mjs +3 -2
- package/src/commands/$.pwd.mjs +1 -1
- package/src/shell-parser.mjs +375 -0
package/src/commands/$.cd.mjs
CHANGED
|
@@ -8,9 +8,10 @@ export default async function cd({ args }) {
|
|
|
8
8
|
process.chdir(target);
|
|
9
9
|
const newDir = process.cwd();
|
|
10
10
|
trace('VirtualCommand', () => `cd: success | ${JSON.stringify({ newDir }, null, 2)}`);
|
|
11
|
-
|
|
11
|
+
// cd command should not output anything on success, just like real cd
|
|
12
|
+
return VirtualUtils.success('');
|
|
12
13
|
} catch (error) {
|
|
13
14
|
trace('VirtualCommand', () => `cd: failed | ${JSON.stringify({ error: error.message }, null, 2)}`);
|
|
14
|
-
return { stderr: `cd: ${error.message}`, code: 1 };
|
|
15
|
+
return { stderr: `cd: ${error.message}\n`, code: 1 };
|
|
15
16
|
}
|
|
16
17
|
}
|
package/src/commands/$.pwd.mjs
CHANGED
|
@@ -4,5 +4,5 @@ export default async function pwd({ args, stdin, cwd }) {
|
|
|
4
4
|
// If cwd option is provided, return that instead of process.cwd()
|
|
5
5
|
const dir = cwd || process.cwd();
|
|
6
6
|
trace('VirtualCommand', () => `pwd: getting directory | ${JSON.stringify({ dir }, null, 2)}`);
|
|
7
|
-
return VirtualUtils.success(dir);
|
|
7
|
+
return VirtualUtils.success(dir + '\n');
|
|
8
8
|
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced shell command parser that handles &&, ||, ;, and () operators
|
|
3
|
+
* This allows virtual commands to work properly with shell operators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { trace } from './$.utils.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Token types for the parser
|
|
10
|
+
*/
|
|
11
|
+
const TokenType = {
|
|
12
|
+
WORD: 'word',
|
|
13
|
+
AND: '&&',
|
|
14
|
+
OR: '||',
|
|
15
|
+
SEMICOLON: ';',
|
|
16
|
+
PIPE: '|',
|
|
17
|
+
LPAREN: '(',
|
|
18
|
+
RPAREN: ')',
|
|
19
|
+
REDIRECT_OUT: '>',
|
|
20
|
+
REDIRECT_APPEND: '>>',
|
|
21
|
+
REDIRECT_IN: '<',
|
|
22
|
+
EOF: 'eof'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tokenize a shell command string
|
|
27
|
+
*/
|
|
28
|
+
function tokenize(command) {
|
|
29
|
+
const tokens = [];
|
|
30
|
+
let i = 0;
|
|
31
|
+
|
|
32
|
+
while (i < command.length) {
|
|
33
|
+
// Skip whitespace
|
|
34
|
+
while (i < command.length && /\s/.test(command[i])) {
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (i >= command.length) break;
|
|
39
|
+
|
|
40
|
+
// Check for operators
|
|
41
|
+
if (command[i] === '&' && command[i + 1] === '&') {
|
|
42
|
+
tokens.push({ type: TokenType.AND, value: '&&' });
|
|
43
|
+
i += 2;
|
|
44
|
+
} else if (command[i] === '|' && command[i + 1] === '|') {
|
|
45
|
+
tokens.push({ type: TokenType.OR, value: '||' });
|
|
46
|
+
i += 2;
|
|
47
|
+
} else if (command[i] === '|') {
|
|
48
|
+
tokens.push({ type: TokenType.PIPE, value: '|' });
|
|
49
|
+
i++;
|
|
50
|
+
} else if (command[i] === ';') {
|
|
51
|
+
tokens.push({ type: TokenType.SEMICOLON, value: ';' });
|
|
52
|
+
i++;
|
|
53
|
+
} else if (command[i] === '(') {
|
|
54
|
+
tokens.push({ type: TokenType.LPAREN, value: '(' });
|
|
55
|
+
i++;
|
|
56
|
+
} else if (command[i] === ')') {
|
|
57
|
+
tokens.push({ type: TokenType.RPAREN, value: ')' });
|
|
58
|
+
i++;
|
|
59
|
+
} else if (command[i] === '>' && command[i + 1] === '>') {
|
|
60
|
+
tokens.push({ type: TokenType.REDIRECT_APPEND, value: '>>' });
|
|
61
|
+
i += 2;
|
|
62
|
+
} else if (command[i] === '>') {
|
|
63
|
+
tokens.push({ type: TokenType.REDIRECT_OUT, value: '>' });
|
|
64
|
+
i++;
|
|
65
|
+
} else if (command[i] === '<') {
|
|
66
|
+
tokens.push({ type: TokenType.REDIRECT_IN, value: '<' });
|
|
67
|
+
i++;
|
|
68
|
+
} else {
|
|
69
|
+
// Parse word (respecting quotes)
|
|
70
|
+
let word = '';
|
|
71
|
+
let inQuote = false;
|
|
72
|
+
let quoteChar = '';
|
|
73
|
+
|
|
74
|
+
while (i < command.length) {
|
|
75
|
+
const char = command[i];
|
|
76
|
+
|
|
77
|
+
if (!inQuote) {
|
|
78
|
+
if (char === '"' || char === "'") {
|
|
79
|
+
inQuote = true;
|
|
80
|
+
quoteChar = char;
|
|
81
|
+
word += char;
|
|
82
|
+
i++;
|
|
83
|
+
} else if (/\s/.test(char) ||
|
|
84
|
+
'&|;()<>'.includes(char)) {
|
|
85
|
+
break;
|
|
86
|
+
} else if (char === '\\' && i + 1 < command.length) {
|
|
87
|
+
// Handle escape sequences
|
|
88
|
+
word += char;
|
|
89
|
+
i++;
|
|
90
|
+
if (i < command.length) {
|
|
91
|
+
word += command[i];
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
word += char;
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
if (char === quoteChar && command[i - 1] !== '\\') {
|
|
100
|
+
inQuote = false;
|
|
101
|
+
quoteChar = '';
|
|
102
|
+
word += char;
|
|
103
|
+
i++;
|
|
104
|
+
} else if (char === '\\' && i + 1 < command.length &&
|
|
105
|
+
(command[i + 1] === quoteChar || command[i + 1] === '\\')) {
|
|
106
|
+
// Handle escaped quotes and backslashes inside quotes
|
|
107
|
+
word += char;
|
|
108
|
+
i++;
|
|
109
|
+
if (i < command.length) {
|
|
110
|
+
word += command[i];
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
word += char;
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (word) {
|
|
121
|
+
tokens.push({ type: TokenType.WORD, value: word });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
tokens.push({ type: TokenType.EOF, value: '' });
|
|
127
|
+
return tokens;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse a sequence of commands with operators
|
|
132
|
+
*/
|
|
133
|
+
class ShellParser {
|
|
134
|
+
constructor(command) {
|
|
135
|
+
this.tokens = tokenize(command);
|
|
136
|
+
this.pos = 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
current() {
|
|
140
|
+
return this.tokens[this.pos] || { type: TokenType.EOF, value: '' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
peek() {
|
|
144
|
+
return this.tokens[this.pos + 1] || { type: TokenType.EOF, value: '' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
consume() {
|
|
148
|
+
const token = this.current();
|
|
149
|
+
this.pos++;
|
|
150
|
+
return token;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parse the main command sequence
|
|
155
|
+
*/
|
|
156
|
+
parse() {
|
|
157
|
+
return this.parseSequence();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Parse a sequence of commands connected by &&, ||, ;
|
|
162
|
+
*/
|
|
163
|
+
parseSequence() {
|
|
164
|
+
const commands = [];
|
|
165
|
+
const operators = [];
|
|
166
|
+
|
|
167
|
+
// Parse first command
|
|
168
|
+
let cmd = this.parsePipeline();
|
|
169
|
+
if (cmd) {
|
|
170
|
+
commands.push(cmd);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Parse additional commands with operators
|
|
174
|
+
while (this.current().type !== TokenType.EOF &&
|
|
175
|
+
this.current().type !== TokenType.RPAREN) {
|
|
176
|
+
const op = this.current();
|
|
177
|
+
|
|
178
|
+
if (op.type === TokenType.AND ||
|
|
179
|
+
op.type === TokenType.OR ||
|
|
180
|
+
op.type === TokenType.SEMICOLON) {
|
|
181
|
+
operators.push(op.type);
|
|
182
|
+
this.consume();
|
|
183
|
+
|
|
184
|
+
cmd = this.parsePipeline();
|
|
185
|
+
if (cmd) {
|
|
186
|
+
commands.push(cmd);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (commands.length === 1 && operators.length === 0) {
|
|
194
|
+
return commands[0];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
type: 'sequence',
|
|
199
|
+
commands,
|
|
200
|
+
operators
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parse a pipeline (commands connected by |)
|
|
206
|
+
*/
|
|
207
|
+
parsePipeline() {
|
|
208
|
+
const commands = [];
|
|
209
|
+
|
|
210
|
+
let cmd = this.parseCommand();
|
|
211
|
+
if (cmd) {
|
|
212
|
+
commands.push(cmd);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
while (this.current().type === TokenType.PIPE) {
|
|
216
|
+
this.consume();
|
|
217
|
+
cmd = this.parseCommand();
|
|
218
|
+
if (cmd) {
|
|
219
|
+
commands.push(cmd);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (commands.length === 1) {
|
|
224
|
+
return commands[0];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
type: 'pipeline',
|
|
229
|
+
commands
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parse a single command or subshell
|
|
235
|
+
*/
|
|
236
|
+
parseCommand() {
|
|
237
|
+
// Check for subshell
|
|
238
|
+
if (this.current().type === TokenType.LPAREN) {
|
|
239
|
+
this.consume(); // consume (
|
|
240
|
+
const subshell = this.parseSequence();
|
|
241
|
+
|
|
242
|
+
if (this.current().type === TokenType.RPAREN) {
|
|
243
|
+
this.consume(); // consume )
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
type: 'subshell',
|
|
248
|
+
command: subshell
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Parse simple command
|
|
253
|
+
return this.parseSimpleCommand();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Parse a simple command (command + args + redirections)
|
|
258
|
+
*/
|
|
259
|
+
parseSimpleCommand() {
|
|
260
|
+
const words = [];
|
|
261
|
+
const redirects = [];
|
|
262
|
+
|
|
263
|
+
while (this.current().type !== TokenType.EOF) {
|
|
264
|
+
const token = this.current();
|
|
265
|
+
|
|
266
|
+
if (token.type === TokenType.WORD) {
|
|
267
|
+
words.push(token.value);
|
|
268
|
+
this.consume();
|
|
269
|
+
} else if (token.type === TokenType.REDIRECT_OUT ||
|
|
270
|
+
token.type === TokenType.REDIRECT_APPEND ||
|
|
271
|
+
token.type === TokenType.REDIRECT_IN) {
|
|
272
|
+
this.consume();
|
|
273
|
+
const target = this.current();
|
|
274
|
+
if (target.type === TokenType.WORD) {
|
|
275
|
+
redirects.push({
|
|
276
|
+
type: token.type,
|
|
277
|
+
target: target.value
|
|
278
|
+
});
|
|
279
|
+
this.consume();
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (words.length === 0) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const cmd = words[0];
|
|
291
|
+
const args = words.slice(1).map(word => {
|
|
292
|
+
// Remove quotes if present
|
|
293
|
+
if ((word.startsWith('"') && word.endsWith('"')) ||
|
|
294
|
+
(word.startsWith("'") && word.endsWith("'"))) {
|
|
295
|
+
return {
|
|
296
|
+
value: word.slice(1, -1),
|
|
297
|
+
quoted: true,
|
|
298
|
+
quoteChar: word[0]
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
value: word,
|
|
303
|
+
quoted: false
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const result = {
|
|
308
|
+
type: 'simple',
|
|
309
|
+
cmd,
|
|
310
|
+
args
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (redirects.length > 0) {
|
|
314
|
+
result.redirects = redirects;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parse a shell command with support for &&, ||, ;, and ()
|
|
323
|
+
*/
|
|
324
|
+
export function parseShellCommand(command) {
|
|
325
|
+
try {
|
|
326
|
+
const parser = new ShellParser(command);
|
|
327
|
+
const result = parser.parse();
|
|
328
|
+
|
|
329
|
+
trace('ShellParser', () => `Parsed command | ${JSON.stringify({
|
|
330
|
+
input: command.slice(0, 100),
|
|
331
|
+
result
|
|
332
|
+
}, null, 2)}`);
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
trace('ShellParser', () => `Parse error | ${JSON.stringify({
|
|
337
|
+
command: command.slice(0, 100),
|
|
338
|
+
error: error.message
|
|
339
|
+
}, null, 2)}`);
|
|
340
|
+
|
|
341
|
+
// Return null to fallback to sh -c
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Check if a command needs shell features we don't handle
|
|
348
|
+
*/
|
|
349
|
+
export function needsRealShell(command) {
|
|
350
|
+
// Check for features we don't handle yet
|
|
351
|
+
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
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
for (const feature of unsupported) {
|
|
367
|
+
if (command.includes(feature)) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export default { parseShellCommand, needsRealShell };
|