command-stream 0.9.2 → 0.9.4
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 +63 -0
- package/js/BEST-PRACTICES.md +376 -0
- package/js/docs/IMPLEMENTATION_NOTES.md +129 -0
- package/js/docs/SHELL_OPERATORS_IMPLEMENTATION.md +101 -0
- package/js/docs/case-studies/issue-144/README.md +215 -0
- package/js/docs/case-studies/issue-144/failures-summary.md +161 -0
- package/js/docs/case-studies/issue-146/README.md +244 -0
- package/js/docs/case-studies/issue-153/README.md +167 -0
- package/js/docs/shell-operators-implementation.md +97 -0
- package/js/tests/array-interpolation.test.mjs +329 -0
- package/package.json +1 -1
- package/rust/BEST-PRACTICES.md +382 -0
package/README.md
CHANGED
|
@@ -1334,6 +1334,69 @@ const quickResult = $`pwd`.sync();
|
|
|
1334
1334
|
$`npm install`.on('stdout', showProgress).start();
|
|
1335
1335
|
```
|
|
1336
1336
|
|
|
1337
|
+
## Common Pitfalls
|
|
1338
|
+
|
|
1339
|
+
### Array Argument Handling
|
|
1340
|
+
|
|
1341
|
+
When passing multiple arguments, pass the array directly - **never use `.join(' ')`** before interpolation:
|
|
1342
|
+
|
|
1343
|
+
```javascript
|
|
1344
|
+
import { $ } from 'command-stream';
|
|
1345
|
+
|
|
1346
|
+
// WRONG - entire string becomes ONE argument
|
|
1347
|
+
const args = ['file.txt', '--public', '--verbose'];
|
|
1348
|
+
await $`command ${args.join(' ')}`;
|
|
1349
|
+
// Shell receives: command 'file.txt --public --verbose' (1 argument!)
|
|
1350
|
+
// Error: File does not exist: "file.txt --public --verbose"
|
|
1351
|
+
|
|
1352
|
+
// CORRECT - each element becomes separate argument
|
|
1353
|
+
await $`command ${args}`;
|
|
1354
|
+
// Shell receives: command file.txt --public --verbose (3 arguments)
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
This is a common mistake that causes errors like:
|
|
1358
|
+
|
|
1359
|
+
```
|
|
1360
|
+
Error: File does not exist: "/path/to/file.txt --flag --option"
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
### Why This Happens
|
|
1364
|
+
|
|
1365
|
+
The `$` template tag handles arrays specially - each element is quoted separately:
|
|
1366
|
+
|
|
1367
|
+
```javascript
|
|
1368
|
+
if (Array.isArray(value)) {
|
|
1369
|
+
return value.map(quote).join(' '); // Each element quoted individually
|
|
1370
|
+
}
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
But when you call `.join(' ')` first:
|
|
1374
|
+
|
|
1375
|
+
1. The array becomes a string: `"file.txt --public --verbose"`
|
|
1376
|
+
2. Template receives a **string**, not an array
|
|
1377
|
+
3. The entire string gets quoted as one argument
|
|
1378
|
+
4. Command receives one argument containing spaces
|
|
1379
|
+
|
|
1380
|
+
### Recommended Patterns
|
|
1381
|
+
|
|
1382
|
+
```javascript
|
|
1383
|
+
// Pattern 1: Direct array passing
|
|
1384
|
+
const args = ['file.txt', '--verbose'];
|
|
1385
|
+
await $`command ${args}`;
|
|
1386
|
+
|
|
1387
|
+
// Pattern 2: Separate interpolations
|
|
1388
|
+
const file = 'file.txt';
|
|
1389
|
+
const flags = ['--verbose', '--force'];
|
|
1390
|
+
await $`command ${file} ${flags}`;
|
|
1391
|
+
|
|
1392
|
+
// Pattern 3: Build array dynamically
|
|
1393
|
+
const allArgs = ['input.txt'];
|
|
1394
|
+
if (verbose) allArgs.push('--verbose');
|
|
1395
|
+
await $`command ${allArgs}`;
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
See [js/BEST-PRACTICES.md](js/BEST-PRACTICES.md) for more detailed guidance.
|
|
1399
|
+
|
|
1337
1400
|
## Testing
|
|
1338
1401
|
|
|
1339
1402
|
```bash
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Best Practices for command-stream
|
|
2
|
+
|
|
3
|
+
This document covers best practices, common patterns, and pitfalls to avoid when using the command-stream library.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Array Argument Handling](#array-argument-handling)
|
|
8
|
+
- [String Interpolation](#string-interpolation)
|
|
9
|
+
- [Security Best Practices](#security-best-practices)
|
|
10
|
+
- [Error Handling](#error-handling)
|
|
11
|
+
- [Performance Tips](#performance-tips)
|
|
12
|
+
- [Common Pitfalls](#common-pitfalls)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Array Argument Handling
|
|
17
|
+
|
|
18
|
+
### Pass Arrays Directly
|
|
19
|
+
|
|
20
|
+
When you have multiple arguments in an array, pass the array directly to template interpolation. The library will automatically handle proper quoting for each element.
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
import { $ } from 'command-stream';
|
|
24
|
+
|
|
25
|
+
// CORRECT: Pass array directly
|
|
26
|
+
const args = ['file.txt', '--public', '--verbose'];
|
|
27
|
+
await $`command ${args}`;
|
|
28
|
+
// Executed: command file.txt --public --verbose
|
|
29
|
+
|
|
30
|
+
// CORRECT: Dynamic array building
|
|
31
|
+
const baseArgs = ['input.txt'];
|
|
32
|
+
if (isVerbose) baseArgs.push('--verbose');
|
|
33
|
+
if (isForce) baseArgs.push('--force');
|
|
34
|
+
await $`mycommand ${baseArgs}`;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Never Use .join() Before Interpolation
|
|
38
|
+
|
|
39
|
+
Calling `.join(' ')` on an array before passing to template interpolation is a common mistake that causes all elements to become a single argument.
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// WRONG: Array becomes single argument
|
|
43
|
+
const args = ['file.txt', '--flag'];
|
|
44
|
+
await $`command ${args.join(' ')}`;
|
|
45
|
+
// Shell receives: ['command', 'file.txt --flag'] (1 argument!)
|
|
46
|
+
|
|
47
|
+
// CORRECT: Each element becomes separate argument
|
|
48
|
+
await $`command ${args}`;
|
|
49
|
+
// Shell receives: ['command', 'file.txt', '--flag'] (2 arguments)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Mixed Static and Dynamic Arguments
|
|
53
|
+
|
|
54
|
+
When combining static and dynamic arguments, use separate interpolations or arrays:
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
// CORRECT: Multiple interpolations
|
|
58
|
+
const file = 'data.txt';
|
|
59
|
+
const flags = ['--verbose', '--force'];
|
|
60
|
+
await $`process ${file} ${flags}`;
|
|
61
|
+
|
|
62
|
+
// CORRECT: Build complete array
|
|
63
|
+
const allArgs = [file, ...flags];
|
|
64
|
+
await $`process ${allArgs}`;
|
|
65
|
+
|
|
66
|
+
// WRONG: String concatenation
|
|
67
|
+
await $`process ${file + ' ' + flags.join(' ')}`;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## String Interpolation
|
|
73
|
+
|
|
74
|
+
### Safe Interpolation (Default)
|
|
75
|
+
|
|
76
|
+
By default, all interpolated values are automatically quoted to prevent shell injection:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
// User input is safely escaped
|
|
80
|
+
const userInput = "'; rm -rf /; echo '";
|
|
81
|
+
await $`echo ${userInput}`;
|
|
82
|
+
// Executed safely - input is quoted, not executed
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Using raw() for Trusted Commands
|
|
86
|
+
|
|
87
|
+
Only use `raw()` with trusted, hardcoded command strings:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
import { $, raw } from 'command-stream';
|
|
91
|
+
|
|
92
|
+
// CORRECT: Trusted command template
|
|
93
|
+
const trustedCmd = 'git log --oneline --graph';
|
|
94
|
+
await $`${raw(trustedCmd)}`;
|
|
95
|
+
|
|
96
|
+
// WRONG: User input with raw (security vulnerability!)
|
|
97
|
+
const userInput = req.body.command;
|
|
98
|
+
await $`${raw(userInput)}`; // DANGER: Shell injection!
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Paths with Spaces
|
|
102
|
+
|
|
103
|
+
Paths containing spaces are automatically quoted:
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
const path = '/Users/name/My Documents/file.txt';
|
|
107
|
+
await $`cat ${path}`;
|
|
108
|
+
// Executed: cat '/Users/name/My Documents/file.txt'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Security Best Practices
|
|
114
|
+
|
|
115
|
+
### Never Trust User Input
|
|
116
|
+
|
|
117
|
+
Always treat external input as potentially malicious:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
// CORRECT: Auto-escaping protects against injection
|
|
121
|
+
const filename = req.query.file;
|
|
122
|
+
await $`cat ${filename}`;
|
|
123
|
+
|
|
124
|
+
// WRONG: Bypassing safety for user input
|
|
125
|
+
await $`${raw(userInput)}`;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Validate Before Execution
|
|
129
|
+
|
|
130
|
+
Add validation for critical operations:
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
import { $ } from 'command-stream';
|
|
134
|
+
|
|
135
|
+
async function deleteFile(filename) {
|
|
136
|
+
// Validate filename
|
|
137
|
+
if (filename.includes('..') || filename.startsWith('/')) {
|
|
138
|
+
throw new Error('Invalid filename');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await $`rm ${filename}`;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Use Principle of Least Privilege
|
|
146
|
+
|
|
147
|
+
Run commands with minimal required permissions:
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
// Use specific paths instead of wildcards when possible
|
|
151
|
+
await $`rm ${specificFile}`; // Better
|
|
152
|
+
await $`rm ${directory}/*`; // More risky
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Error Handling
|
|
158
|
+
|
|
159
|
+
### Check Exit Codes
|
|
160
|
+
|
|
161
|
+
By default, commands don't throw on non-zero exit codes:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
const result = await $`ls nonexistent`;
|
|
165
|
+
if (result.code !== 0) {
|
|
166
|
+
console.error('Command failed:', result.stderr);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Enable errexit for Critical Operations
|
|
171
|
+
|
|
172
|
+
Use shell settings for scripts that should fail on errors:
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
import { $, shell } from 'command-stream';
|
|
176
|
+
|
|
177
|
+
shell.errexit(true);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await $`critical-operation`;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('Critical operation failed:', error);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Handle Specific Errors
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
const result = await $`command`;
|
|
191
|
+
|
|
192
|
+
switch (result.code) {
|
|
193
|
+
case 0:
|
|
194
|
+
console.log('Success:', result.stdout);
|
|
195
|
+
break;
|
|
196
|
+
case 1:
|
|
197
|
+
console.error('General error');
|
|
198
|
+
break;
|
|
199
|
+
case 127:
|
|
200
|
+
console.error('Command not found');
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
console.error(`Unknown error (code ${result.code})`);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Performance Tips
|
|
210
|
+
|
|
211
|
+
### Use Streaming for Large Outputs
|
|
212
|
+
|
|
213
|
+
For commands that produce large outputs, use streaming to avoid memory issues:
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
// Memory efficient: Process chunks as they arrive
|
|
217
|
+
for await (const chunk of $`cat huge-file.log`.stream()) {
|
|
218
|
+
processChunk(chunk.data);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Memory intensive: Buffers entire output
|
|
222
|
+
const result = await $`cat huge-file.log`;
|
|
223
|
+
processAll(result.stdout);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Parallel Execution
|
|
227
|
+
|
|
228
|
+
Run independent commands in parallel:
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
// Sequential (slower)
|
|
232
|
+
await $`task1`;
|
|
233
|
+
await $`task2`;
|
|
234
|
+
await $`task3`;
|
|
235
|
+
|
|
236
|
+
// Parallel (faster)
|
|
237
|
+
await Promise.all([$`task1`, $`task2`, $`task3`]);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Use Built-in Commands
|
|
241
|
+
|
|
242
|
+
Built-in commands are faster as they don't spawn system processes:
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
// Fast: Built-in command (pure JavaScript)
|
|
246
|
+
await $`mkdir -p build/output`;
|
|
247
|
+
|
|
248
|
+
// Slower: System command
|
|
249
|
+
await $`/bin/mkdir -p build/output`;
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Common Pitfalls
|
|
255
|
+
|
|
256
|
+
### 1. Array.join() Pitfall (Most Common)
|
|
257
|
+
|
|
258
|
+
**Problem:** Using `.join(' ')` before interpolation merges all arguments into one.
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
// WRONG
|
|
262
|
+
const args = ['file.txt', '--flag'];
|
|
263
|
+
await $`cmd ${args.join(' ')}`; // 1 argument: "file.txt --flag"
|
|
264
|
+
|
|
265
|
+
// CORRECT
|
|
266
|
+
await $`cmd ${args}`; // 2 arguments: "file.txt", "--flag"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
See [Case Study: Issue #153](./docs/case-studies/issue-153/README.md) for detailed analysis.
|
|
270
|
+
|
|
271
|
+
### 2. Template String Concatenation
|
|
272
|
+
|
|
273
|
+
**Problem:** Building commands with template strings creates single arguments.
|
|
274
|
+
|
|
275
|
+
```javascript
|
|
276
|
+
// WRONG
|
|
277
|
+
const file = 'data.txt';
|
|
278
|
+
const flag = '--verbose';
|
|
279
|
+
await $`cmd ${`${file} ${flag}`}`; // 1 argument: "data.txt --verbose"
|
|
280
|
+
|
|
281
|
+
// CORRECT
|
|
282
|
+
await $`cmd ${file} ${flag}`; // 2 arguments
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 3. Forgetting await
|
|
286
|
+
|
|
287
|
+
**Problem:** Commands return promises, forgetting await causes issues.
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
// WRONG: Command may not complete before next line
|
|
291
|
+
$`setup-task`;
|
|
292
|
+
$`main-task`; // May run before setup completes
|
|
293
|
+
|
|
294
|
+
// CORRECT: Wait for completion
|
|
295
|
+
await $`setup-task`;
|
|
296
|
+
await $`main-task`;
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 4. Assuming Synchronous Behavior
|
|
300
|
+
|
|
301
|
+
**Problem:** Expecting immediate results without awaiting.
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
// WRONG
|
|
305
|
+
const cmd = $`echo hello`;
|
|
306
|
+
console.log(cmd.stdout); // undefined - not yet executed!
|
|
307
|
+
|
|
308
|
+
// CORRECT
|
|
309
|
+
const result = await $`echo hello`;
|
|
310
|
+
console.log(result.stdout); // "hello\n"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### 5. Not Handling stderr
|
|
314
|
+
|
|
315
|
+
**Problem:** Only checking stdout when errors go to stderr.
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
// INCOMPLETE
|
|
319
|
+
const result = await $`command`;
|
|
320
|
+
console.log(result.stdout);
|
|
321
|
+
|
|
322
|
+
// BETTER
|
|
323
|
+
const result = await $`command`;
|
|
324
|
+
if (result.code !== 0) {
|
|
325
|
+
console.error('Error:', result.stderr);
|
|
326
|
+
} else {
|
|
327
|
+
console.log('Success:', result.stdout);
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### 6. Ignoring Exit Codes
|
|
332
|
+
|
|
333
|
+
**Problem:** Assuming success without checking.
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
// WRONG
|
|
337
|
+
const result = await $`risky-command`;
|
|
338
|
+
processOutput(result.stdout); // May be empty on failure!
|
|
339
|
+
|
|
340
|
+
// CORRECT
|
|
341
|
+
const result = await $`risky-command`;
|
|
342
|
+
if (result.code === 0) {
|
|
343
|
+
processOutput(result.stdout);
|
|
344
|
+
} else {
|
|
345
|
+
handleError(result);
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Quick Reference
|
|
352
|
+
|
|
353
|
+
### Do's
|
|
354
|
+
|
|
355
|
+
- Pass arrays directly: `${args}`
|
|
356
|
+
- Use separate interpolations: `${file} ${flag}`
|
|
357
|
+
- Check exit codes after execution
|
|
358
|
+
- Use streaming for large outputs
|
|
359
|
+
- Validate user input before execution
|
|
360
|
+
- Use built-in commands when available
|
|
361
|
+
|
|
362
|
+
### Don'ts
|
|
363
|
+
|
|
364
|
+
- Never use `args.join(' ')` before interpolation
|
|
365
|
+
- Never use `raw()` with user input
|
|
366
|
+
- Don't forget `await` on commands
|
|
367
|
+
- Don't assume success without checking
|
|
368
|
+
- Don't ignore stderr output
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## See Also
|
|
373
|
+
|
|
374
|
+
- [README.md](../README.md) - Main documentation
|
|
375
|
+
- [docs/case-studies/issue-153/README.md](./docs/case-studies/issue-153/README.md) - Array.join() pitfall case study
|
|
376
|
+
- [src/$.quote.mjs](./src/$.quote.mjs) - Quote function implementation
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Implementation Notes for Shell Operators Support
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
The current implementation passes commands with `&&`, `||`, `;`, `()` to `sh -c`, which runs them in a subprocess. This prevents the virtual `cd` command from affecting the parent process directory.
|
|
6
|
+
|
|
7
|
+
## Solution
|
|
8
|
+
|
|
9
|
+
We've created a shell parser (`src/shell-parser.mjs`) that can parse these operators. Now we need to integrate it into `src/$.mjs` to execute parsed commands through our virtual command system.
|
|
10
|
+
|
|
11
|
+
## Required Changes in src/$.mjs
|
|
12
|
+
|
|
13
|
+
### 1. Import the parser
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { parseShellCommand, needsRealShell } from './shell-parser.mjs';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Modify \_doStartAsync method
|
|
20
|
+
|
|
21
|
+
Before checking for pipes, check if the command contains operators we can handle:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
async _doStartAsync() {
|
|
25
|
+
// ... existing code ...
|
|
26
|
+
|
|
27
|
+
// Check if command needs parsing for operators
|
|
28
|
+
if (this.spec.mode === 'shell' && !needsRealShell(this.spec.command)) {
|
|
29
|
+
const parsed = parseShellCommand(this.spec.command);
|
|
30
|
+
if (parsed && parsed.type === 'sequence') {
|
|
31
|
+
return await this._runSequence(parsed);
|
|
32
|
+
} else if (parsed && parsed.type === 'subshell') {
|
|
33
|
+
return await this._runSubshell(parsed);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ... continue with existing pipeline check ...
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Add \_runSequence method
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
async _runSequence(sequence) {
|
|
45
|
+
let lastResult = { code: 0, stdout: '', stderr: '' };
|
|
46
|
+
let combinedStdout = '';
|
|
47
|
+
let combinedStderr = '';
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < sequence.commands.length; i++) {
|
|
50
|
+
const command = sequence.commands[i];
|
|
51
|
+
const operator = i > 0 ? sequence.operators[i - 1] : null;
|
|
52
|
+
|
|
53
|
+
// Check operator conditions
|
|
54
|
+
if (operator === '&&' && lastResult.code !== 0) continue;
|
|
55
|
+
if (operator === '||' && lastResult.code === 0) continue;
|
|
56
|
+
|
|
57
|
+
// Execute command
|
|
58
|
+
if (command.type === 'subshell') {
|
|
59
|
+
lastResult = await this._runSubshell(command);
|
|
60
|
+
} else if (command.type === 'pipeline') {
|
|
61
|
+
lastResult = await this._runPipeline(command.commands);
|
|
62
|
+
} else if (command.type === 'simple') {
|
|
63
|
+
lastResult = await this._runSimpleCommand(command);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
combinedStdout += lastResult.stdout;
|
|
67
|
+
combinedStderr += lastResult.stderr;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
code: lastResult.code,
|
|
72
|
+
stdout: combinedStdout,
|
|
73
|
+
stderr: combinedStderr
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 4. Add \_runSubshell method
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
async _runSubshell(subshell) {
|
|
82
|
+
// Save current directory
|
|
83
|
+
const savedCwd = process.cwd();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Execute subshell command
|
|
87
|
+
const result = await this._runSequence(subshell.command);
|
|
88
|
+
return result;
|
|
89
|
+
} finally {
|
|
90
|
+
// Restore directory
|
|
91
|
+
process.chdir(savedCwd);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 5. Add \_runSimpleCommand method
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
async _runSimpleCommand(command) {
|
|
100
|
+
const { cmd, args, redirects } = command;
|
|
101
|
+
|
|
102
|
+
// Check for virtual command
|
|
103
|
+
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
104
|
+
const argValues = args.map(a => a.value);
|
|
105
|
+
return await this._runVirtual(cmd, argValues);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fall back to real command execution
|
|
109
|
+
// ... handle redirects if present ...
|
|
110
|
+
|
|
111
|
+
return await this._executeRealCommand(cmd, args);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Testing
|
|
116
|
+
|
|
117
|
+
After implementation:
|
|
118
|
+
|
|
119
|
+
1. `cd /tmp && pwd` should output `/tmp`
|
|
120
|
+
2. `cd /tmp` followed by `pwd` should output `/tmp`
|
|
121
|
+
3. `(cd /tmp && pwd) ; pwd` should output `/tmp` then original directory
|
|
122
|
+
4. All existing tests should still pass
|
|
123
|
+
|
|
124
|
+
## Benefits
|
|
125
|
+
|
|
126
|
+
1. Virtual commands work with shell operators
|
|
127
|
+
2. `cd` behaves exactly like shell cd
|
|
128
|
+
3. Better performance (no subprocess for simple operator chains)
|
|
129
|
+
4. Consistent behavior across platforms
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Shell Operators Implementation Complete
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
We've successfully implemented support for shell operators (`&&`, `||`, `;`, `()`) in command-stream, allowing virtual commands like `cd` to work correctly with these operators.
|
|
6
|
+
|
|
7
|
+
## What Was Implemented
|
|
8
|
+
|
|
9
|
+
### 1. Shell Parser (`src/shell-parser.mjs`)
|
|
10
|
+
|
|
11
|
+
- Tokenizes and parses shell commands with operators
|
|
12
|
+
- Handles `&&` (AND), `||` (OR), `;` (semicolon), `()` (subshells)
|
|
13
|
+
- Supports pipes `|`, redirections `>`, `>>`, `<`
|
|
14
|
+
- Properly handles quoted strings and escaped characters
|
|
15
|
+
- Falls back to `sh -c` for unsupported features (globs, variable expansion, etc.)
|
|
16
|
+
|
|
17
|
+
### 2. ProcessRunner Enhancements (`src/$.mjs`)
|
|
18
|
+
|
|
19
|
+
- `_runSequence()` - Executes command sequences with proper operator semantics
|
|
20
|
+
- `_runSubshell()` - Handles subshell isolation (saves/restores cwd)
|
|
21
|
+
- `_runSimpleCommand()` - Executes individual commands (virtual or real)
|
|
22
|
+
- Integration with existing pipeline support
|
|
23
|
+
|
|
24
|
+
### 3. Fixed cd Command Behavior
|
|
25
|
+
|
|
26
|
+
- `cd` now works correctly in all contexts:
|
|
27
|
+
- `cd /tmp` - changes directory (persists) ✓
|
|
28
|
+
- `cd /tmp && ls` - both commands see /tmp ✓
|
|
29
|
+
- `(cd /tmp && ls)` - subshell isolation ✓
|
|
30
|
+
- `cd /tmp ; pwd ; cd /usr ; pwd` - sequential execution ✓
|
|
31
|
+
|
|
32
|
+
## How It Works
|
|
33
|
+
|
|
34
|
+
1. When a command contains operators, the enhanced parser parses it into an AST
|
|
35
|
+
2. The executor traverses the AST, respecting operator semantics:
|
|
36
|
+
- `&&` - run next only if previous succeeds (exit code 0)
|
|
37
|
+
- `||` - run next only if previous fails (exit code ≠ 0)
|
|
38
|
+
- `;` - run next regardless
|
|
39
|
+
- `()` - run in subshell with saved/restored directory
|
|
40
|
+
3. Virtual commands execute in-process, maintaining state
|
|
41
|
+
4. Real commands spawn subprocesses as needed
|
|
42
|
+
5. Falls back to `sh -c` for unsupported features
|
|
43
|
+
|
|
44
|
+
## Examples That Now Work
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
// cd with && operator
|
|
48
|
+
await $`cd /tmp && pwd`; // Output: /tmp
|
|
49
|
+
|
|
50
|
+
// cd with || operator
|
|
51
|
+
await $`cd /nonexistent || echo "failed"`; // Output: failed
|
|
52
|
+
|
|
53
|
+
// Multiple commands with ;
|
|
54
|
+
await $`cd /tmp ; pwd ; cd /usr ; pwd`; // Output: /tmp\n/usr
|
|
55
|
+
|
|
56
|
+
// Subshell isolation
|
|
57
|
+
await $`(cd /tmp && pwd) ; pwd`; // Output: /tmp\n<original-dir>
|
|
58
|
+
|
|
59
|
+
// Complex chains
|
|
60
|
+
await $`cd /tmp && git init && echo "done"`;
|
|
61
|
+
|
|
62
|
+
// Nested subshells
|
|
63
|
+
await $`(cd /tmp && (cd /usr && pwd) && pwd)`; // Output: /usr\n/tmp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Benefits
|
|
67
|
+
|
|
68
|
+
1. **Correct Shell Semantics** - cd and other virtual commands behave exactly like in a real shell
|
|
69
|
+
2. **Performance** - No subprocess overhead for simple command chains
|
|
70
|
+
3. **Cross-platform** - Consistent behavior across platforms
|
|
71
|
+
4. **Backward Compatible** - Existing code continues to work
|
|
72
|
+
5. **Graceful Fallback** - Complex shell features still work via `sh -c`
|
|
73
|
+
|
|
74
|
+
## Testing
|
|
75
|
+
|
|
76
|
+
All major shell operator scenarios are tested and working:
|
|
77
|
+
|
|
78
|
+
- ✓ AND operator (`&&`)
|
|
79
|
+
- ✓ OR operator (`||`)
|
|
80
|
+
- ✓ Semicolon operator (`;`)
|
|
81
|
+
- ✓ Subshells (`()`)
|
|
82
|
+
- ✓ Nested subshells
|
|
83
|
+
- ✓ Complex command chains
|
|
84
|
+
- ✓ Directory persistence
|
|
85
|
+
- ✓ Path with spaces
|
|
86
|
+
|
|
87
|
+
## Files Modified
|
|
88
|
+
|
|
89
|
+
1. `src/$.mjs` - Added sequence/subshell execution methods
|
|
90
|
+
2. `src/shell-parser.mjs` - New file for parsing shell operators
|
|
91
|
+
3. `src/commands/$.pwd.mjs` - Fixed to output newline
|
|
92
|
+
4. Various test and example files
|
|
93
|
+
|
|
94
|
+
## Future Enhancements
|
|
95
|
+
|
|
96
|
+
Possible future additions:
|
|
97
|
+
|
|
98
|
+
- Background execution (`&`)
|
|
99
|
+
- Here documents (`<<`)
|
|
100
|
+
- Process substitution (`<()`, `>()`)
|
|
101
|
+
- More complex redirections (`2>&1`, etc.)
|