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
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# Best Practices for command-stream (Rust)
|
|
2
|
+
|
|
3
|
+
This document covers best practices, common patterns, and pitfalls to avoid when using the command-stream Rust library.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Argument Handling with Macros](#argument-handling-with-macros)
|
|
8
|
+
- [String Interpolation](#string-interpolation)
|
|
9
|
+
- [Security Best Practices](#security-best-practices)
|
|
10
|
+
- [Error Handling](#error-handling)
|
|
11
|
+
- [Async Patterns](#async-patterns)
|
|
12
|
+
- [Common Pitfalls](#common-pitfalls)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Argument Handling with Macros
|
|
17
|
+
|
|
18
|
+
### Using the cmd!/s!/sh! Macros
|
|
19
|
+
|
|
20
|
+
The command-stream macros (`cmd!`, `s!`, `sh!`, `cs!`) provide safe interpolation similar to JavaScript's `$` template literal:
|
|
21
|
+
|
|
22
|
+
```rust
|
|
23
|
+
use command_stream::s;
|
|
24
|
+
|
|
25
|
+
#[tokio::main]
|
|
26
|
+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
27
|
+
// Simple command
|
|
28
|
+
let result = s!("echo hello world").await?;
|
|
29
|
+
|
|
30
|
+
// With interpolation (automatically quoted)
|
|
31
|
+
let name = "world";
|
|
32
|
+
let result = s!("echo hello {}", name).await?;
|
|
33
|
+
|
|
34
|
+
// Multiple arguments
|
|
35
|
+
let file = "test.txt";
|
|
36
|
+
let flag = "--verbose";
|
|
37
|
+
let result = s!("cat {} {}", file, flag).await?;
|
|
38
|
+
|
|
39
|
+
Ok(())
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Handling Multiple Arguments
|
|
44
|
+
|
|
45
|
+
When you have a collection of arguments, handle them correctly:
|
|
46
|
+
|
|
47
|
+
```rust
|
|
48
|
+
use command_stream::{run, quote};
|
|
49
|
+
|
|
50
|
+
// CORRECT: Use quote::quote_all for multiple arguments
|
|
51
|
+
let args = vec!["file.txt", "--public", "--verbose"];
|
|
52
|
+
let quoted_args = quote::quote_all(&args);
|
|
53
|
+
let result = run(format!("command {}", quoted_args)).await?;
|
|
54
|
+
|
|
55
|
+
// CORRECT: Build command with individual quotes
|
|
56
|
+
let args = vec!["file.txt", "--public", "--verbose"];
|
|
57
|
+
let cmd = format!("command {}",
|
|
58
|
+
args.iter()
|
|
59
|
+
.map(|a| quote::quote(a))
|
|
60
|
+
.collect::<Vec<_>>()
|
|
61
|
+
.join(" ")
|
|
62
|
+
);
|
|
63
|
+
let result = run(cmd).await?;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Vec/Slice Handling Patterns
|
|
67
|
+
|
|
68
|
+
Unlike JavaScript where arrays are handled automatically in template literals, Rust requires explicit handling:
|
|
69
|
+
|
|
70
|
+
```rust
|
|
71
|
+
use command_stream::quote;
|
|
72
|
+
|
|
73
|
+
// Pattern 1: quote_all function
|
|
74
|
+
let args = vec!["file.txt", "--verbose"];
|
|
75
|
+
let args_str = quote::quote_all(&args);
|
|
76
|
+
// Result: "file.txt --verbose" (each arg properly quoted)
|
|
77
|
+
|
|
78
|
+
// Pattern 2: Manual iteration
|
|
79
|
+
let args = vec!["file with spaces.txt", "--verbose"];
|
|
80
|
+
let args_str = args.iter()
|
|
81
|
+
.map(|a| quote::quote(a))
|
|
82
|
+
.collect::<Vec<_>>()
|
|
83
|
+
.join(" ");
|
|
84
|
+
// Result: "'file with spaces.txt' --verbose"
|
|
85
|
+
|
|
86
|
+
// Pattern 3: Format with multiple placeholders
|
|
87
|
+
let file = "data.txt";
|
|
88
|
+
let flag1 = "--verbose";
|
|
89
|
+
let flag2 = "--force";
|
|
90
|
+
let result = s!("cmd {} {} {}", file, flag1, flag2).await?;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## String Interpolation
|
|
96
|
+
|
|
97
|
+
### Safe Interpolation (Default)
|
|
98
|
+
|
|
99
|
+
The `quote` function automatically escapes dangerous characters:
|
|
100
|
+
|
|
101
|
+
```rust
|
|
102
|
+
use command_stream::quote::quote;
|
|
103
|
+
|
|
104
|
+
let dangerous = "'; rm -rf /; echo '";
|
|
105
|
+
let safe = quote(dangerous);
|
|
106
|
+
// Result: "''\\'' rm -rf /; echo '\\'''"
|
|
107
|
+
// Shell will treat this as a literal string
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### When Quoting is Applied
|
|
111
|
+
|
|
112
|
+
```rust
|
|
113
|
+
use command_stream::quote::{quote, needs_quoting};
|
|
114
|
+
|
|
115
|
+
// Safe strings pass through unchanged
|
|
116
|
+
assert_eq!(quote("hello"), "hello");
|
|
117
|
+
assert_eq!(quote("/path/to/file"), "/path/to/file");
|
|
118
|
+
|
|
119
|
+
// Dangerous strings are quoted
|
|
120
|
+
assert_eq!(quote("hello world"), "'hello world'");
|
|
121
|
+
assert_eq!(quote("$var"), "'$var'");
|
|
122
|
+
|
|
123
|
+
// Check if quoting is needed
|
|
124
|
+
assert!(!needs_quoting("hello"));
|
|
125
|
+
assert!(needs_quoting("hello world"));
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Security Best Practices
|
|
131
|
+
|
|
132
|
+
### Never Trust User Input
|
|
133
|
+
|
|
134
|
+
```rust
|
|
135
|
+
use command_stream::{run, quote};
|
|
136
|
+
|
|
137
|
+
async fn process_file(user_filename: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
138
|
+
// CORRECT: Quote user input
|
|
139
|
+
let safe_filename = quote::quote(user_filename);
|
|
140
|
+
let result = run(format!("cat {}", safe_filename)).await?;
|
|
141
|
+
|
|
142
|
+
// ALSO CORRECT: Use macro interpolation
|
|
143
|
+
let result = s!("cat {}", user_filename).await?;
|
|
144
|
+
|
|
145
|
+
Ok(())
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Validate Before Execution
|
|
150
|
+
|
|
151
|
+
```rust
|
|
152
|
+
use command_stream::run;
|
|
153
|
+
use std::path::Path;
|
|
154
|
+
|
|
155
|
+
async fn delete_file(filename: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
156
|
+
// Validate: no path traversal
|
|
157
|
+
if filename.contains("..") || filename.starts_with('/') {
|
|
158
|
+
return Err("Invalid filename".into());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validate: file exists and is a file (not directory)
|
|
162
|
+
let path = Path::new(filename);
|
|
163
|
+
if !path.is_file() {
|
|
164
|
+
return Err("Not a file".into());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
run(format!("rm {}", quote::quote(filename))).await?;
|
|
168
|
+
Ok(())
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Error Handling
|
|
175
|
+
|
|
176
|
+
### Check Results
|
|
177
|
+
|
|
178
|
+
```rust
|
|
179
|
+
use command_stream::run;
|
|
180
|
+
|
|
181
|
+
async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
|
182
|
+
let result = run("ls nonexistent").await?;
|
|
183
|
+
|
|
184
|
+
match result.code {
|
|
185
|
+
0 => println!("Success: {}", result.stdout),
|
|
186
|
+
2 => eprintln!("File not found"),
|
|
187
|
+
127 => eprintln!("Command not found"),
|
|
188
|
+
code => eprintln!("Unknown error (code {})", code),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Ok(())
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Using the ? Operator
|
|
196
|
+
|
|
197
|
+
```rust
|
|
198
|
+
use command_stream::{run, Result};
|
|
199
|
+
|
|
200
|
+
async fn critical_operation() -> Result<String> {
|
|
201
|
+
let result = run("important-command").await?;
|
|
202
|
+
|
|
203
|
+
if result.code != 0 {
|
|
204
|
+
return Err(command_stream::Error::CommandFailed {
|
|
205
|
+
code: result.code,
|
|
206
|
+
message: result.stderr,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
Ok(result.stdout)
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Async Patterns
|
|
217
|
+
|
|
218
|
+
### Basic Async Usage
|
|
219
|
+
|
|
220
|
+
```rust
|
|
221
|
+
use command_stream::run;
|
|
222
|
+
|
|
223
|
+
#[tokio::main]
|
|
224
|
+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
225
|
+
let result = run("echo hello").await?;
|
|
226
|
+
println!("{}", result.stdout);
|
|
227
|
+
Ok(())
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Parallel Execution
|
|
232
|
+
|
|
233
|
+
```rust
|
|
234
|
+
use command_stream::run;
|
|
235
|
+
use tokio;
|
|
236
|
+
|
|
237
|
+
async fn parallel_tasks() -> Result<(), Box<dyn std::error::Error>> {
|
|
238
|
+
// Run multiple commands in parallel
|
|
239
|
+
let (r1, r2, r3) = tokio::join!(
|
|
240
|
+
run("task1"),
|
|
241
|
+
run("task2"),
|
|
242
|
+
run("task3")
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
println!("Task 1: {}", r1?.stdout);
|
|
246
|
+
println!("Task 2: {}", r2?.stdout);
|
|
247
|
+
println!("Task 3: {}", r3?.stdout);
|
|
248
|
+
|
|
249
|
+
Ok(())
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Using ProcessRunner for Control
|
|
254
|
+
|
|
255
|
+
```rust
|
|
256
|
+
use command_stream::{ProcessRunner, RunOptions};
|
|
257
|
+
|
|
258
|
+
async fn controlled_execution() -> Result<(), Box<dyn std::error::Error>> {
|
|
259
|
+
let options = RunOptions {
|
|
260
|
+
capture: true,
|
|
261
|
+
mirror: false, // Don't print to terminal
|
|
262
|
+
..Default::default()
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
let mut runner = ProcessRunner::new("long-command", options);
|
|
266
|
+
runner.start().await?;
|
|
267
|
+
let result = runner.run().await?;
|
|
268
|
+
|
|
269
|
+
println!("Captured: {}", result.stdout);
|
|
270
|
+
Ok(())
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Common Pitfalls
|
|
277
|
+
|
|
278
|
+
### 1. String Formatting Without Quoting
|
|
279
|
+
|
|
280
|
+
**Problem:** Using `format!` without quoting can cause issues with special characters.
|
|
281
|
+
|
|
282
|
+
```rust
|
|
283
|
+
// WRONG: Spaces break the command
|
|
284
|
+
let filename = "my file.txt";
|
|
285
|
+
let cmd = format!("cat {}", filename);
|
|
286
|
+
// Result: "cat my file.txt" - interpreted as two args!
|
|
287
|
+
|
|
288
|
+
// CORRECT: Quote the value
|
|
289
|
+
let cmd = format!("cat {}", quote::quote(filename));
|
|
290
|
+
// Result: "cat 'my file.txt'" - single argument
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 2. Vec Join Without Proper Quoting
|
|
294
|
+
|
|
295
|
+
**Problem:** Joining a Vec without quoting each element.
|
|
296
|
+
|
|
297
|
+
```rust
|
|
298
|
+
// WRONG: join doesn't quote elements
|
|
299
|
+
let args = vec!["file with spaces.txt", "--flag"];
|
|
300
|
+
let cmd = format!("command {}", args.join(" "));
|
|
301
|
+
// Result: "command file with spaces.txt --flag" - BROKEN!
|
|
302
|
+
|
|
303
|
+
// CORRECT: Use quote_all
|
|
304
|
+
let cmd = format!("command {}", quote::quote_all(&args));
|
|
305
|
+
// Result: "command 'file with spaces.txt' --flag"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### 3. Forgetting .await
|
|
309
|
+
|
|
310
|
+
**Problem:** Async functions return futures that must be awaited.
|
|
311
|
+
|
|
312
|
+
```rust
|
|
313
|
+
// WRONG: Command never executes
|
|
314
|
+
let result = run("echo hello"); // Returns Future, not Result!
|
|
315
|
+
|
|
316
|
+
// CORRECT: Await the future
|
|
317
|
+
let result = run("echo hello").await?;
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### 4. Not Handling Non-Zero Exit Codes
|
|
321
|
+
|
|
322
|
+
**Problem:** Assuming success without checking.
|
|
323
|
+
|
|
324
|
+
```rust
|
|
325
|
+
// RISKY: May fail silently
|
|
326
|
+
let result = run("risky-command").await?;
|
|
327
|
+
use_output(&result.stdout);
|
|
328
|
+
|
|
329
|
+
// BETTER: Check exit code
|
|
330
|
+
let result = run("risky-command").await?;
|
|
331
|
+
if result.code == 0 {
|
|
332
|
+
use_output(&result.stdout);
|
|
333
|
+
} else {
|
|
334
|
+
handle_error(&result.stderr);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### 5. Blocking in Async Context
|
|
339
|
+
|
|
340
|
+
**Problem:** Using `run_sync` in async context blocks the runtime.
|
|
341
|
+
|
|
342
|
+
```rust
|
|
343
|
+
// WRONG in async context
|
|
344
|
+
async fn bad_example() {
|
|
345
|
+
// This blocks the entire runtime thread!
|
|
346
|
+
let result = run_sync("slow-command");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// CORRECT: Use async version
|
|
350
|
+
async fn good_example() {
|
|
351
|
+
let result = run("slow-command").await;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Quick Reference
|
|
358
|
+
|
|
359
|
+
### Do's
|
|
360
|
+
|
|
361
|
+
- Use `quote::quote()` for individual values
|
|
362
|
+
- Use `quote::quote_all()` for Vec/slice of arguments
|
|
363
|
+
- Use macro interpolation (`s!`, `cmd!`) for safe templating
|
|
364
|
+
- Always `.await` async operations
|
|
365
|
+
- Check exit codes for critical operations
|
|
366
|
+
- Validate user input before execution
|
|
367
|
+
|
|
368
|
+
### Don'ts
|
|
369
|
+
|
|
370
|
+
- Never format user input without quoting
|
|
371
|
+
- Never use `args.join(" ")` without quoting each element
|
|
372
|
+
- Don't forget `.await` on futures
|
|
373
|
+
- Don't assume commands succeed
|
|
374
|
+
- Don't block async contexts with `run_sync`
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## See Also
|
|
379
|
+
|
|
380
|
+
- [../js/BEST-PRACTICES.md](../js/BEST-PRACTICES.md) - JavaScript best practices
|
|
381
|
+
- [src/quote.rs](src/quote.rs) - Quote function implementation
|
|
382
|
+
- [src/macros.rs](src/macros.rs) - Macro implementations
|