command-stream 0.9.0 → 0.9.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/js/src/$.ansi.mjs +147 -0
- package/js/src/$.mjs +49 -6382
- package/js/src/$.process-runner-base.mjs +563 -0
- package/js/src/$.process-runner-execution.mjs +1497 -0
- package/js/src/$.process-runner-orchestration.mjs +250 -0
- package/js/src/$.process-runner-pipeline.mjs +1162 -0
- package/js/src/$.process-runner-stream-kill.mjs +312 -0
- package/js/src/$.process-runner-virtual.mjs +297 -0
- package/js/src/$.quote.mjs +161 -0
- package/js/src/$.result.mjs +23 -0
- package/js/src/$.shell-settings.mjs +84 -0
- package/js/src/$.shell.mjs +157 -0
- package/js/src/$.state.mjs +401 -0
- package/js/src/$.stream-emitter.mjs +111 -0
- package/js/src/$.stream-utils.mjs +390 -0
- package/js/src/$.trace.mjs +36 -0
- package/js/src/$.utils.mjs +2 -23
- package/js/src/$.virtual-commands.mjs +113 -0
- package/js/src/commands/$.which.mjs +3 -1
- package/js/src/commands/index.mjs +24 -0
- package/js/src/shell-parser.mjs +125 -83
- package/js/tests/resource-cleanup-internals.test.mjs +22 -24
- package/js/tests/sigint-cleanup.test.mjs +3 -0
- package/package.json +1 -1
- package/rust/src/ansi.rs +194 -0
- package/rust/src/events.rs +305 -0
- package/rust/src/lib.rs +71 -60
- package/rust/src/macros.rs +165 -0
- package/rust/src/pipeline.rs +411 -0
- package/rust/src/quote.rs +161 -0
- package/rust/src/state.rs +333 -0
- package/rust/src/stream.rs +369 -0
- package/rust/src/trace.rs +152 -0
- package/rust/src/utils.rs +53 -158
- package/rust/tests/events.rs +207 -0
- package/rust/tests/macros.rs +77 -0
- package/rust/tests/pipeline.rs +93 -0
- package/rust/tests/state.rs +207 -0
- package/rust/tests/stream.rs +102 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
//! Event emitter for stream events
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides an EventEmitter-like implementation for ProcessRunner
|
|
4
|
+
//! events, similar to the JavaScript StreamEmitter class.
|
|
5
|
+
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
use tokio::sync::RwLock;
|
|
9
|
+
|
|
10
|
+
use crate::trace::trace_lazy;
|
|
11
|
+
|
|
12
|
+
/// Event types that can be emitted by ProcessRunner
|
|
13
|
+
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
|
14
|
+
pub enum EventType {
|
|
15
|
+
/// Stdout data received
|
|
16
|
+
Stdout,
|
|
17
|
+
/// Stderr data received
|
|
18
|
+
Stderr,
|
|
19
|
+
/// Combined data event (contains type and data)
|
|
20
|
+
Data,
|
|
21
|
+
/// Process ended
|
|
22
|
+
End,
|
|
23
|
+
/// Process exited with code
|
|
24
|
+
Exit,
|
|
25
|
+
/// Error occurred
|
|
26
|
+
Error,
|
|
27
|
+
/// Process spawned
|
|
28
|
+
Spawn,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl std::fmt::Display for EventType {
|
|
32
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
33
|
+
match self {
|
|
34
|
+
EventType::Stdout => write!(f, "stdout"),
|
|
35
|
+
EventType::Stderr => write!(f, "stderr"),
|
|
36
|
+
EventType::Data => write!(f, "data"),
|
|
37
|
+
EventType::End => write!(f, "end"),
|
|
38
|
+
EventType::Exit => write!(f, "exit"),
|
|
39
|
+
EventType::Error => write!(f, "error"),
|
|
40
|
+
EventType::Spawn => write!(f, "spawn"),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Event data variants
|
|
46
|
+
#[derive(Debug, Clone)]
|
|
47
|
+
pub enum EventData {
|
|
48
|
+
/// String data (for stdout, stderr)
|
|
49
|
+
String(String),
|
|
50
|
+
/// Exit code
|
|
51
|
+
ExitCode(i32),
|
|
52
|
+
/// Data event with type and content
|
|
53
|
+
TypedData { data_type: String, data: String },
|
|
54
|
+
/// Command result
|
|
55
|
+
Result(crate::CommandResult),
|
|
56
|
+
/// Error message
|
|
57
|
+
Error(String),
|
|
58
|
+
/// No data
|
|
59
|
+
None,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Type alias for event listeners
|
|
63
|
+
type Listener = Arc<dyn Fn(EventData) + Send + Sync>;
|
|
64
|
+
|
|
65
|
+
/// Event emitter for ProcessRunner events
|
|
66
|
+
///
|
|
67
|
+
/// Provides on(), once(), off(), and emit() methods similar to Node.js EventEmitter.
|
|
68
|
+
pub struct StreamEmitter {
|
|
69
|
+
listeners: RwLock<HashMap<EventType, Vec<Listener>>>,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl Default for StreamEmitter {
|
|
73
|
+
fn default() -> Self {
|
|
74
|
+
Self::new()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl StreamEmitter {
|
|
79
|
+
/// Create a new event emitter
|
|
80
|
+
pub fn new() -> Self {
|
|
81
|
+
StreamEmitter {
|
|
82
|
+
listeners: RwLock::new(HashMap::new()),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Register a listener for an event
|
|
87
|
+
///
|
|
88
|
+
/// # Arguments
|
|
89
|
+
/// * `event` - The event type to listen for
|
|
90
|
+
/// * `listener` - The callback function to invoke
|
|
91
|
+
///
|
|
92
|
+
/// # Example
|
|
93
|
+
/// ```ignore
|
|
94
|
+
/// emitter.on(EventType::Stdout, |data| {
|
|
95
|
+
/// if let EventData::String(s) = data {
|
|
96
|
+
/// println!("Got stdout: {}", s);
|
|
97
|
+
/// }
|
|
98
|
+
/// });
|
|
99
|
+
/// ```
|
|
100
|
+
pub async fn on<F>(&self, event: EventType, listener: F)
|
|
101
|
+
where
|
|
102
|
+
F: Fn(EventData) + Send + Sync + 'static,
|
|
103
|
+
{
|
|
104
|
+
trace_lazy("StreamEmitter", || {
|
|
105
|
+
format!("on() called for event: {}", event)
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
let mut listeners = self.listeners.write().await;
|
|
109
|
+
listeners
|
|
110
|
+
.entry(event)
|
|
111
|
+
.or_default()
|
|
112
|
+
.push(Arc::new(listener));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Register a one-time listener for an event
|
|
116
|
+
///
|
|
117
|
+
/// The listener will be removed after it is invoked once.
|
|
118
|
+
pub async fn once<F>(&self, event: EventType, listener: F)
|
|
119
|
+
where
|
|
120
|
+
F: Fn(EventData) + Send + Sync + 'static,
|
|
121
|
+
{
|
|
122
|
+
trace_lazy("StreamEmitter", || {
|
|
123
|
+
format!("once() called for event: {}", event)
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Wrap the listener to track if it's been called
|
|
127
|
+
let called = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
128
|
+
let called_clone = called.clone();
|
|
129
|
+
|
|
130
|
+
let once_listener = move |data: EventData| {
|
|
131
|
+
if !called_clone.swap(true, std::sync::atomic::Ordering::SeqCst) {
|
|
132
|
+
listener(data);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
self.on(event, once_listener).await;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Emit an event to all registered listeners
|
|
140
|
+
///
|
|
141
|
+
/// # Arguments
|
|
142
|
+
/// * `event` - The event type to emit
|
|
143
|
+
/// * `data` - The event data to pass to listeners
|
|
144
|
+
pub async fn emit(&self, event: EventType, data: EventData) {
|
|
145
|
+
let listeners = self.listeners.read().await;
|
|
146
|
+
|
|
147
|
+
if let Some(event_listeners) = listeners.get(&event) {
|
|
148
|
+
trace_lazy("StreamEmitter", || {
|
|
149
|
+
format!(
|
|
150
|
+
"Emitting event {} to {} listeners",
|
|
151
|
+
event,
|
|
152
|
+
event_listeners.len()
|
|
153
|
+
)
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
for listener in event_listeners {
|
|
157
|
+
listener(data.clone());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Remove all listeners for an event
|
|
163
|
+
///
|
|
164
|
+
/// # Arguments
|
|
165
|
+
/// * `event` - The event type to clear listeners for
|
|
166
|
+
pub async fn off(&self, event: EventType) {
|
|
167
|
+
trace_lazy("StreamEmitter", || {
|
|
168
|
+
format!("off() called for event: {}", event)
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let mut listeners = self.listeners.write().await;
|
|
172
|
+
listeners.remove(&event);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// Get the number of listeners for an event
|
|
176
|
+
pub async fn listener_count(&self, event: &EventType) -> usize {
|
|
177
|
+
let listeners = self.listeners.read().await;
|
|
178
|
+
listeners.get(event).map(|v| v.len()).unwrap_or(0)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Remove all listeners for all events
|
|
182
|
+
pub async fn remove_all_listeners(&self) {
|
|
183
|
+
trace_lazy("StreamEmitter", || "Removing all listeners".to_string());
|
|
184
|
+
let mut listeners = self.listeners.write().await;
|
|
185
|
+
listeners.clear();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
impl std::fmt::Debug for StreamEmitter {
|
|
190
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
191
|
+
f.debug_struct("StreamEmitter")
|
|
192
|
+
.field("listeners", &"<RwLock<HashMap<...>>>")
|
|
193
|
+
.finish()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[cfg(test)]
|
|
198
|
+
mod tests {
|
|
199
|
+
use super::*;
|
|
200
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
201
|
+
|
|
202
|
+
#[tokio::test]
|
|
203
|
+
async fn test_emit_basic() {
|
|
204
|
+
let emitter = StreamEmitter::new();
|
|
205
|
+
let counter = Arc::new(AtomicUsize::new(0));
|
|
206
|
+
let counter_clone = counter.clone();
|
|
207
|
+
|
|
208
|
+
emitter
|
|
209
|
+
.on(EventType::Stdout, move |_| {
|
|
210
|
+
counter_clone.fetch_add(1, Ordering::SeqCst);
|
|
211
|
+
})
|
|
212
|
+
.await;
|
|
213
|
+
|
|
214
|
+
emitter
|
|
215
|
+
.emit(EventType::Stdout, EventData::String("test".to_string()))
|
|
216
|
+
.await;
|
|
217
|
+
|
|
218
|
+
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#[tokio::test]
|
|
222
|
+
async fn test_once() {
|
|
223
|
+
let emitter = StreamEmitter::new();
|
|
224
|
+
let counter = Arc::new(AtomicUsize::new(0));
|
|
225
|
+
let counter_clone = counter.clone();
|
|
226
|
+
|
|
227
|
+
emitter
|
|
228
|
+
.once(EventType::Exit, move |_| {
|
|
229
|
+
counter_clone.fetch_add(1, Ordering::SeqCst);
|
|
230
|
+
})
|
|
231
|
+
.await;
|
|
232
|
+
|
|
233
|
+
// Emit twice
|
|
234
|
+
emitter.emit(EventType::Exit, EventData::ExitCode(0)).await;
|
|
235
|
+
emitter.emit(EventType::Exit, EventData::ExitCode(0)).await;
|
|
236
|
+
|
|
237
|
+
// Should only be called once
|
|
238
|
+
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[tokio::test]
|
|
242
|
+
async fn test_off() {
|
|
243
|
+
let emitter = StreamEmitter::new();
|
|
244
|
+
let counter = Arc::new(AtomicUsize::new(0));
|
|
245
|
+
let counter_clone = counter.clone();
|
|
246
|
+
|
|
247
|
+
emitter
|
|
248
|
+
.on(EventType::Stdout, move |_| {
|
|
249
|
+
counter_clone.fetch_add(1, Ordering::SeqCst);
|
|
250
|
+
})
|
|
251
|
+
.await;
|
|
252
|
+
|
|
253
|
+
emitter.off(EventType::Stdout).await;
|
|
254
|
+
emitter
|
|
255
|
+
.emit(EventType::Stdout, EventData::String("test".to_string()))
|
|
256
|
+
.await;
|
|
257
|
+
|
|
258
|
+
assert_eq!(counter.load(Ordering::SeqCst), 0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[tokio::test]
|
|
262
|
+
async fn test_listener_count() {
|
|
263
|
+
let emitter = StreamEmitter::new();
|
|
264
|
+
|
|
265
|
+
assert_eq!(emitter.listener_count(&EventType::Stdout).await, 0);
|
|
266
|
+
|
|
267
|
+
emitter.on(EventType::Stdout, |_| {}).await;
|
|
268
|
+
assert_eq!(emitter.listener_count(&EventType::Stdout).await, 1);
|
|
269
|
+
|
|
270
|
+
emitter.on(EventType::Stdout, |_| {}).await;
|
|
271
|
+
assert_eq!(emitter.listener_count(&EventType::Stdout).await, 2);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[tokio::test]
|
|
275
|
+
async fn test_multiple_events() {
|
|
276
|
+
let emitter = StreamEmitter::new();
|
|
277
|
+
let stdout_counter = Arc::new(AtomicUsize::new(0));
|
|
278
|
+
let stderr_counter = Arc::new(AtomicUsize::new(0));
|
|
279
|
+
|
|
280
|
+
let stdout_clone = stdout_counter.clone();
|
|
281
|
+
let stderr_clone = stderr_counter.clone();
|
|
282
|
+
|
|
283
|
+
emitter
|
|
284
|
+
.on(EventType::Stdout, move |_| {
|
|
285
|
+
stdout_clone.fetch_add(1, Ordering::SeqCst);
|
|
286
|
+
})
|
|
287
|
+
.await;
|
|
288
|
+
|
|
289
|
+
emitter
|
|
290
|
+
.on(EventType::Stderr, move |_| {
|
|
291
|
+
stderr_clone.fetch_add(1, Ordering::SeqCst);
|
|
292
|
+
})
|
|
293
|
+
.await;
|
|
294
|
+
|
|
295
|
+
emitter
|
|
296
|
+
.emit(EventType::Stdout, EventData::String("out".to_string()))
|
|
297
|
+
.await;
|
|
298
|
+
emitter
|
|
299
|
+
.emit(EventType::Stderr, EventData::String("err".to_string()))
|
|
300
|
+
.await;
|
|
301
|
+
|
|
302
|
+
assert_eq!(stdout_counter.load(Ordering::SeqCst), 1);
|
|
303
|
+
assert_eq!(stderr_counter.load(Ordering::SeqCst), 1);
|
|
304
|
+
}
|
|
305
|
+
}
|
package/rust/src/lib.rs
CHANGED
|
@@ -9,40 +9,93 @@
|
|
|
9
9
|
//!
|
|
10
10
|
//! - Async command execution with tokio
|
|
11
11
|
//! - Streaming output via async iterators
|
|
12
|
+
//! - Event-based output handling (on, once, emit)
|
|
12
13
|
//! - Virtual commands for common operations (cat, ls, mkdir, etc.)
|
|
13
14
|
//! - Shell operator support (&&, ||, ;, |)
|
|
15
|
+
//! - Pipeline support with `.pipe()` method and `Pipeline` builder
|
|
16
|
+
//! - Global state management for shell settings
|
|
17
|
+
//! - `cmd!` macro for ergonomic command creation (similar to JS `$` tagged template literals)
|
|
14
18
|
//! - Cross-platform support
|
|
15
19
|
//!
|
|
20
|
+
//! ## Module Organization
|
|
21
|
+
//!
|
|
22
|
+
//! The codebase follows a modular architecture similar to the JavaScript implementation:
|
|
23
|
+
//!
|
|
24
|
+
//! - `ansi` - ANSI escape code handling utilities
|
|
25
|
+
//! - `commands` - Virtual command implementations
|
|
26
|
+
//! - `events` - Event emitter for stream events
|
|
27
|
+
//! - `macros` - The `cmd!` macro for ergonomic command creation
|
|
28
|
+
//! - `pipeline` - Pipeline execution support
|
|
29
|
+
//! - `quote` - Shell quoting utilities
|
|
30
|
+
//! - `shell_parser` - Shell command parsing
|
|
31
|
+
//! - `state` - Global state management
|
|
32
|
+
//! - `stream` - Async streaming and iteration support
|
|
33
|
+
//! - `trace` - Logging and tracing utilities
|
|
34
|
+
//! - `utils` - Command results and virtual command helpers
|
|
35
|
+
//!
|
|
16
36
|
//! ## Quick Start
|
|
17
37
|
//!
|
|
18
38
|
//! ```rust,no_run
|
|
19
|
-
//! use command_stream::run;
|
|
39
|
+
//! use command_stream::{run, cmd};
|
|
20
40
|
//!
|
|
21
41
|
//! #[tokio::main]
|
|
22
42
|
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
23
43
|
//! // Execute a simple command
|
|
24
44
|
//! let result = run("echo hello world").await?;
|
|
25
45
|
//! println!("{}", result.stdout);
|
|
46
|
+
//!
|
|
47
|
+
//! // Using the cmd! macro (similar to JS $ tagged template)
|
|
48
|
+
//! let name = "world";
|
|
49
|
+
//! let result = cmd!("echo hello {}", name).await?;
|
|
50
|
+
//! println!("{}", result.stdout);
|
|
51
|
+
//!
|
|
52
|
+
//! // Using pipelines
|
|
53
|
+
//! use command_stream::Pipeline;
|
|
54
|
+
//! let result = Pipeline::new()
|
|
55
|
+
//! .add("echo hello world")
|
|
56
|
+
//! .add("grep world")
|
|
57
|
+
//! .run()
|
|
58
|
+
//! .await?;
|
|
59
|
+
//!
|
|
26
60
|
//! Ok(())
|
|
27
61
|
//! }
|
|
28
62
|
//! ```
|
|
29
63
|
|
|
64
|
+
// Modular utility modules (following JavaScript modular pattern)
|
|
65
|
+
pub mod ansi;
|
|
66
|
+
pub mod events;
|
|
67
|
+
#[doc(hidden)]
|
|
68
|
+
pub mod macros;
|
|
69
|
+
pub mod pipeline;
|
|
70
|
+
pub mod quote;
|
|
71
|
+
pub mod state;
|
|
72
|
+
pub mod stream;
|
|
73
|
+
pub mod trace;
|
|
74
|
+
|
|
75
|
+
// Core modules
|
|
30
76
|
pub mod commands;
|
|
31
77
|
pub mod shell_parser;
|
|
32
78
|
pub mod utils;
|
|
33
79
|
|
|
34
80
|
use std::collections::HashMap;
|
|
35
|
-
use std::env;
|
|
36
81
|
use std::path::PathBuf;
|
|
37
82
|
use std::process::Stdio;
|
|
38
|
-
use std::sync::Arc;
|
|
39
83
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
40
84
|
use tokio::process::{Child, Command};
|
|
41
|
-
use tokio::sync::
|
|
85
|
+
use tokio::sync::mpsc;
|
|
42
86
|
|
|
43
87
|
pub use commands::{CommandContext, StreamChunk};
|
|
44
88
|
pub use shell_parser::{parse_shell_command, needs_real_shell, ParsedCommand};
|
|
45
|
-
pub use utils::{
|
|
89
|
+
pub use utils::{CommandResult, VirtualUtils};
|
|
90
|
+
|
|
91
|
+
// Re-export modular utilities at crate root for convenient access
|
|
92
|
+
pub use ansi::{AnsiConfig, AnsiUtils};
|
|
93
|
+
pub use events::{EventData, EventType, StreamEmitter};
|
|
94
|
+
pub use pipeline::{Pipeline, PipelineExt, PipelineBuilder};
|
|
95
|
+
pub use quote::quote;
|
|
96
|
+
pub use state::{global_state, reset_global_state, get_shell_settings, set_shell_option, unset_shell_option, GlobalState, ShellSettings};
|
|
97
|
+
pub use stream::{StreamingRunner, OutputStream, OutputChunk, AsyncIterator, IntoStream};
|
|
98
|
+
pub use trace::trace;
|
|
46
99
|
|
|
47
100
|
/// Error type for command-stream operations
|
|
48
101
|
#[derive(Debug, thiserror::Error)]
|
|
@@ -66,21 +119,6 @@ pub enum Error {
|
|
|
66
119
|
/// Result type for command-stream operations
|
|
67
120
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
68
121
|
|
|
69
|
-
/// Shell settings for controlling execution behavior
|
|
70
|
-
#[derive(Debug, Clone, Default)]
|
|
71
|
-
pub struct ShellSettings {
|
|
72
|
-
/// Exit immediately if a command exits with non-zero status (set -e)
|
|
73
|
-
pub errexit: bool,
|
|
74
|
-
/// Print commands as they are executed (set -v)
|
|
75
|
-
pub verbose: bool,
|
|
76
|
-
/// Print trace of commands (set -x)
|
|
77
|
-
pub xtrace: bool,
|
|
78
|
-
/// Return value of a pipeline is the status of the last command to exit with non-zero (set -o pipefail)
|
|
79
|
-
pub pipefail: bool,
|
|
80
|
-
/// Treat unset variables as an error (set -u)
|
|
81
|
-
pub nounset: bool,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
122
|
/// Options for command execution
|
|
85
123
|
#[derive(Debug, Clone)]
|
|
86
124
|
pub struct RunOptions {
|
|
@@ -179,8 +217,8 @@ impl ProcessRunner {
|
|
|
179
217
|
return Ok(());
|
|
180
218
|
}
|
|
181
219
|
|
|
182
|
-
// Parse command for shell operators
|
|
183
|
-
let
|
|
220
|
+
// Parse command for shell operators (for future use with virtual command pipelines)
|
|
221
|
+
let _parsed = if self.options.shell_operators && !needs_real_shell(&self.command) {
|
|
184
222
|
parse_shell_command(&self.command)
|
|
185
223
|
} else {
|
|
186
224
|
None
|
|
@@ -364,6 +402,16 @@ impl ProcessRunner {
|
|
|
364
402
|
pub fn result(&self) -> Option<&CommandResult> {
|
|
365
403
|
self.result.as_ref()
|
|
366
404
|
}
|
|
405
|
+
|
|
406
|
+
/// Get the command string
|
|
407
|
+
pub fn command(&self) -> &str {
|
|
408
|
+
&self.command
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/// Get the options
|
|
412
|
+
pub fn options(&self) -> &RunOptions {
|
|
413
|
+
&self.options
|
|
414
|
+
}
|
|
367
415
|
}
|
|
368
416
|
|
|
369
417
|
/// Shell configuration
|
|
@@ -452,41 +500,4 @@ pub fn run_sync(command: impl Into<String>) -> Result<CommandResult> {
|
|
|
452
500
|
rt.block_on(run(command))
|
|
453
501
|
}
|
|
454
502
|
|
|
455
|
-
|
|
456
|
-
mod tests {
|
|
457
|
-
use super::*;
|
|
458
|
-
|
|
459
|
-
#[tokio::test]
|
|
460
|
-
async fn test_simple_echo() {
|
|
461
|
-
let result = run("echo hello").await.unwrap();
|
|
462
|
-
assert!(result.is_success());
|
|
463
|
-
assert!(result.stdout.contains("hello"));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
#[tokio::test]
|
|
467
|
-
async fn test_virtual_echo() {
|
|
468
|
-
let mut runner = ProcessRunner::new("echo test virtual", RunOptions::default());
|
|
469
|
-
let result = runner.run().await.unwrap();
|
|
470
|
-
assert!(result.is_success());
|
|
471
|
-
assert!(result.stdout.contains("test virtual"));
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
#[tokio::test]
|
|
475
|
-
async fn test_process_runner() {
|
|
476
|
-
let mut runner = ProcessRunner::new("echo hello world", RunOptions {
|
|
477
|
-
mirror: false,
|
|
478
|
-
..Default::default()
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
let result = runner.run().await.unwrap();
|
|
482
|
-
assert!(result.is_success());
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
#[tokio::test]
|
|
486
|
-
async fn test_virtual_pwd() {
|
|
487
|
-
let mut runner = ProcessRunner::new("pwd", RunOptions::default());
|
|
488
|
-
let result = runner.run().await.unwrap();
|
|
489
|
-
assert!(result.is_success());
|
|
490
|
-
assert!(!result.stdout.is_empty());
|
|
491
|
-
}
|
|
492
|
-
}
|
|
503
|
+
// Tests are located in tests/ directory for better organization
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
//! Macros for ergonomic command execution
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides command execution macros that offer a similar experience
|
|
4
|
+
//! to JavaScript's `$` tagged template literal for shell command execution.
|
|
5
|
+
//!
|
|
6
|
+
//! ## Available Macros
|
|
7
|
+
//!
|
|
8
|
+
//! - `s!` - Short, concise macro (recommended for most use cases)
|
|
9
|
+
//! - `sh!` - Shell macro (alternative short form)
|
|
10
|
+
//! - `cmd!` - Command macro (explicit name)
|
|
11
|
+
//! - `cs!` - Command-stream macro (another alternative)
|
|
12
|
+
//!
|
|
13
|
+
//! All macros are aliases and provide identical functionality.
|
|
14
|
+
//!
|
|
15
|
+
//! ## Usage
|
|
16
|
+
//!
|
|
17
|
+
//! ```rust,no_run
|
|
18
|
+
//! use command_stream::s;
|
|
19
|
+
//!
|
|
20
|
+
//! #[tokio::main]
|
|
21
|
+
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
22
|
+
//! // Simple command
|
|
23
|
+
//! let result = s!("echo hello world").await?;
|
|
24
|
+
//!
|
|
25
|
+
//! // With interpolation (values are automatically quoted for safety)
|
|
26
|
+
//! let name = "John Doe";
|
|
27
|
+
//! let result = s!("echo Hello, {}", name).await?;
|
|
28
|
+
//!
|
|
29
|
+
//! // Multiple arguments
|
|
30
|
+
//! let file = "test.txt";
|
|
31
|
+
//! let dir = "/tmp";
|
|
32
|
+
//! let result = s!("cp {} {}", file, dir).await?;
|
|
33
|
+
//!
|
|
34
|
+
//! Ok(())
|
|
35
|
+
//! }
|
|
36
|
+
//! ```
|
|
37
|
+
|
|
38
|
+
/// Build a shell command with interpolated values safely quoted
|
|
39
|
+
///
|
|
40
|
+
/// This function is used internally by the `cmd!` macro to build
|
|
41
|
+
/// shell commands with properly quoted interpolated values.
|
|
42
|
+
pub fn build_shell_command(parts: &[&str], values: &[&str]) -> String {
|
|
43
|
+
let mut result = String::new();
|
|
44
|
+
|
|
45
|
+
for (i, part) in parts.iter().enumerate() {
|
|
46
|
+
result.push_str(part);
|
|
47
|
+
if i < values.len() {
|
|
48
|
+
result.push_str(&crate::quote::quote(values[i]));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
result
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Helper function to create a ProcessRunner from a command string
|
|
56
|
+
pub fn create_runner(command: String) -> crate::ProcessRunner {
|
|
57
|
+
crate::ProcessRunner::new(command, crate::RunOptions {
|
|
58
|
+
mirror: true,
|
|
59
|
+
capture: true,
|
|
60
|
+
..Default::default()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Helper function to create a ProcessRunner with custom options
|
|
65
|
+
pub fn create_runner_with_options(command: String, options: crate::RunOptions) -> crate::ProcessRunner {
|
|
66
|
+
crate::ProcessRunner::new(command, options)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// The `cmd!` macro for ergonomic shell command execution
|
|
70
|
+
///
|
|
71
|
+
/// This macro provides a similar experience to JavaScript's `$` tagged template literal.
|
|
72
|
+
/// Values interpolated into the command are automatically quoted for shell safety.
|
|
73
|
+
///
|
|
74
|
+
/// Note: Consider using the shorter `s!` or `sh!` aliases for more concise code.
|
|
75
|
+
///
|
|
76
|
+
/// # Examples
|
|
77
|
+
///
|
|
78
|
+
/// ```rust,no_run
|
|
79
|
+
/// use command_stream::s;
|
|
80
|
+
///
|
|
81
|
+
/// # async fn example() -> Result<(), command_stream::Error> {
|
|
82
|
+
/// // Simple command (returns a future that can be awaited)
|
|
83
|
+
/// let result = s!("echo hello").await?;
|
|
84
|
+
///
|
|
85
|
+
/// // With string interpolation
|
|
86
|
+
/// let name = "world";
|
|
87
|
+
/// let result = s!("echo hello {}", name).await?;
|
|
88
|
+
///
|
|
89
|
+
/// // With multiple values
|
|
90
|
+
/// let src = "source.txt";
|
|
91
|
+
/// let dst = "dest.txt";
|
|
92
|
+
/// let result = s!("cp {} {}", src, dst).await?;
|
|
93
|
+
///
|
|
94
|
+
/// // Values with special characters are automatically quoted
|
|
95
|
+
/// let filename = "file with spaces.txt";
|
|
96
|
+
/// let result = s!("cat {}", filename).await?; // Safely handles spaces
|
|
97
|
+
/// # Ok(())
|
|
98
|
+
/// # }
|
|
99
|
+
/// ```
|
|
100
|
+
///
|
|
101
|
+
/// # Safety
|
|
102
|
+
///
|
|
103
|
+
/// All interpolated values are automatically quoted using shell-safe quoting,
|
|
104
|
+
/// preventing command injection attacks.
|
|
105
|
+
#[macro_export]
|
|
106
|
+
macro_rules! cmd {
|
|
107
|
+
// No interpolation - just a plain command string
|
|
108
|
+
($cmd:expr) => {{
|
|
109
|
+
async {
|
|
110
|
+
$crate::run($cmd).await
|
|
111
|
+
}
|
|
112
|
+
}};
|
|
113
|
+
|
|
114
|
+
// With format-style interpolation
|
|
115
|
+
($fmt:expr, $($arg:expr),+ $(,)?) => {{
|
|
116
|
+
// Build command with quoted values
|
|
117
|
+
let mut result = String::new();
|
|
118
|
+
let values: Vec<String> = vec![$(format!("{}", $arg)),+];
|
|
119
|
+
let values_ref: Vec<&str> = values.iter().map(|s| s.as_str()).collect();
|
|
120
|
+
let fmt_parts: Vec<&str> = $fmt.split("{}").collect();
|
|
121
|
+
for (i, part) in fmt_parts.iter().enumerate() {
|
|
122
|
+
result.push_str(part);
|
|
123
|
+
if i < values_ref.len() {
|
|
124
|
+
result.push_str(&$crate::quote::quote(values_ref[i]));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async move {
|
|
129
|
+
$crate::run(result).await
|
|
130
|
+
}
|
|
131
|
+
}};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// The `sh!` macro - alias for `cmd!`
|
|
135
|
+
///
|
|
136
|
+
/// This is an alternative name for `cmd!` that some users may find
|
|
137
|
+
/// more intuitive for shell command execution.
|
|
138
|
+
#[macro_export]
|
|
139
|
+
macro_rules! sh {
|
|
140
|
+
($($args:tt)*) => {
|
|
141
|
+
$crate::cmd!($($args)*)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// The `s!` macro - short alias for `cmd!`
|
|
146
|
+
///
|
|
147
|
+
/// This is a concise alternative to `cmd!` for quick shell command execution.
|
|
148
|
+
/// Recommended for use in documentation and examples.
|
|
149
|
+
#[macro_export]
|
|
150
|
+
macro_rules! s {
|
|
151
|
+
($($args:tt)*) => {
|
|
152
|
+
$crate::cmd!($($args)*)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// The `cs!` macro - alias for `cmd!`
|
|
157
|
+
///
|
|
158
|
+
/// Short for "command-stream", this provides another alternative
|
|
159
|
+
/// for shell command execution.
|
|
160
|
+
#[macro_export]
|
|
161
|
+
macro_rules! cs {
|
|
162
|
+
($($args:tt)*) => {
|
|
163
|
+
$crate::cmd!($($args)*)
|
|
164
|
+
};
|
|
165
|
+
}
|