command-stream 0.9.1 → 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/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
package/package.json
CHANGED
package/rust/src/ansi.rs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
//! ANSI control character utilities for command-stream
|
|
2
|
+
//!
|
|
3
|
+
//! This module handles stripping and processing of ANSI escape codes
|
|
4
|
+
//! and control characters from text output.
|
|
5
|
+
|
|
6
|
+
/// ANSI control character utilities
|
|
7
|
+
pub struct AnsiUtils;
|
|
8
|
+
|
|
9
|
+
impl AnsiUtils {
|
|
10
|
+
/// Strip ANSI escape sequences from text
|
|
11
|
+
///
|
|
12
|
+
/// Removes color codes, cursor movement, and other ANSI escape sequences
|
|
13
|
+
/// while preserving the actual text content.
|
|
14
|
+
///
|
|
15
|
+
/// # Examples
|
|
16
|
+
///
|
|
17
|
+
/// ```
|
|
18
|
+
/// use command_stream::ansi::AnsiUtils;
|
|
19
|
+
///
|
|
20
|
+
/// let text = "\x1b[31mRed text\x1b[0m";
|
|
21
|
+
/// assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
|
|
22
|
+
/// ```
|
|
23
|
+
pub fn strip_ansi(text: &str) -> String {
|
|
24
|
+
let re = regex::Regex::new(r"\x1b\[[0-9;]*[mGKHFJ]").unwrap();
|
|
25
|
+
re.replace_all(text, "").to_string()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Strip control characters from text, preserving newlines, carriage returns, and tabs
|
|
29
|
+
///
|
|
30
|
+
/// Removes control characters (ASCII 0x00-0x1F and 0x7F) except:
|
|
31
|
+
/// - Newlines (\n = 0x0A)
|
|
32
|
+
/// - Carriage returns (\r = 0x0D)
|
|
33
|
+
/// - Tabs (\t = 0x09)
|
|
34
|
+
///
|
|
35
|
+
/// # Examples
|
|
36
|
+
///
|
|
37
|
+
/// ```
|
|
38
|
+
/// use command_stream::ansi::AnsiUtils;
|
|
39
|
+
///
|
|
40
|
+
/// let text = "Hello\x00World\nNew line\tTab";
|
|
41
|
+
/// assert_eq!(AnsiUtils::strip_control_chars(text), "HelloWorld\nNew line\tTab");
|
|
42
|
+
/// ```
|
|
43
|
+
pub fn strip_control_chars(text: &str) -> String {
|
|
44
|
+
text.chars()
|
|
45
|
+
.filter(|c| {
|
|
46
|
+
// Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09)
|
|
47
|
+
!matches!(*c as u32,
|
|
48
|
+
0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
.collect()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Strip both ANSI sequences and control characters
|
|
55
|
+
///
|
|
56
|
+
/// Combines `strip_ansi` and `strip_control_chars` for complete text cleaning.
|
|
57
|
+
pub fn strip_all(text: &str) -> String {
|
|
58
|
+
Self::strip_control_chars(&Self::strip_ansi(text))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Clean data for processing (strips ANSI and control chars)
|
|
62
|
+
///
|
|
63
|
+
/// Alias for `strip_all` - provides semantic clarity when processing
|
|
64
|
+
/// data that needs to be cleaned for further processing.
|
|
65
|
+
pub fn clean_for_processing(data: &str) -> String {
|
|
66
|
+
Self::strip_all(data)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Configuration for ANSI handling
|
|
71
|
+
///
|
|
72
|
+
/// Controls how ANSI escape codes and control characters are processed
|
|
73
|
+
/// in command output.
|
|
74
|
+
#[derive(Debug, Clone)]
|
|
75
|
+
pub struct AnsiConfig {
|
|
76
|
+
/// Whether to preserve ANSI escape sequences in output
|
|
77
|
+
pub preserve_ansi: bool,
|
|
78
|
+
/// Whether to preserve control characters in output
|
|
79
|
+
pub preserve_control_chars: bool,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl Default for AnsiConfig {
|
|
83
|
+
fn default() -> Self {
|
|
84
|
+
AnsiConfig {
|
|
85
|
+
preserve_ansi: true,
|
|
86
|
+
preserve_control_chars: true,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl AnsiConfig {
|
|
92
|
+
/// Create a new AnsiConfig that preserves everything (default)
|
|
93
|
+
pub fn new() -> Self {
|
|
94
|
+
Self::default()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Create a config that strips all ANSI and control characters
|
|
98
|
+
pub fn strip_all() -> Self {
|
|
99
|
+
AnsiConfig {
|
|
100
|
+
preserve_ansi: false,
|
|
101
|
+
preserve_control_chars: false,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Process output according to config settings
|
|
106
|
+
///
|
|
107
|
+
/// Applies the configured stripping rules to the input data.
|
|
108
|
+
pub fn process_output(&self, data: &str) -> String {
|
|
109
|
+
if !self.preserve_ansi && !self.preserve_control_chars {
|
|
110
|
+
AnsiUtils::clean_for_processing(data)
|
|
111
|
+
} else if !self.preserve_ansi {
|
|
112
|
+
AnsiUtils::strip_ansi(data)
|
|
113
|
+
} else if !self.preserve_control_chars {
|
|
114
|
+
AnsiUtils::strip_control_chars(data)
|
|
115
|
+
} else {
|
|
116
|
+
data.to_string()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[cfg(test)]
|
|
122
|
+
mod tests {
|
|
123
|
+
use super::*;
|
|
124
|
+
|
|
125
|
+
#[test]
|
|
126
|
+
fn test_strip_ansi() {
|
|
127
|
+
let text = "\x1b[31mRed text\x1b[0m";
|
|
128
|
+
assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn test_strip_ansi_multiple_codes() {
|
|
133
|
+
let text = "\x1b[1m\x1b[32mBold Green\x1b[0m Normal";
|
|
134
|
+
assert_eq!(AnsiUtils::strip_ansi(text), "Bold Green Normal");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[test]
|
|
138
|
+
fn test_strip_control_chars() {
|
|
139
|
+
let text = "Hello\x00World\nNew line\tTab";
|
|
140
|
+
assert_eq!(
|
|
141
|
+
AnsiUtils::strip_control_chars(text),
|
|
142
|
+
"HelloWorld\nNew line\tTab"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn test_strip_control_chars_preserves_whitespace() {
|
|
148
|
+
let text = "Line1\nLine2\r\nLine3\tTabbed";
|
|
149
|
+
assert_eq!(
|
|
150
|
+
AnsiUtils::strip_control_chars(text),
|
|
151
|
+
"Line1\nLine2\r\nLine3\tTabbed"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[test]
|
|
156
|
+
fn test_strip_all() {
|
|
157
|
+
let text = "\x1b[31mRed\x00text\x1b[0m";
|
|
158
|
+
assert_eq!(AnsiUtils::strip_all(text), "Redtext");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn test_ansi_config_default() {
|
|
163
|
+
let config = AnsiConfig::default();
|
|
164
|
+
let text = "\x1b[31mRed\x00text\x1b[0m";
|
|
165
|
+
assert_eq!(config.process_output(text), text);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn test_ansi_config_strip_all() {
|
|
170
|
+
let config = AnsiConfig::strip_all();
|
|
171
|
+
let text = "\x1b[31mRed\x00text\x1b[0m";
|
|
172
|
+
assert_eq!(config.process_output(text), "Redtext");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn test_ansi_config_strip_ansi_only() {
|
|
177
|
+
let config = AnsiConfig {
|
|
178
|
+
preserve_ansi: false,
|
|
179
|
+
preserve_control_chars: true,
|
|
180
|
+
};
|
|
181
|
+
let text = "\x1b[31mRed text\x1b[0m";
|
|
182
|
+
assert_eq!(config.process_output(text), "Red text");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn test_ansi_config_strip_control_only() {
|
|
187
|
+
let config = AnsiConfig {
|
|
188
|
+
preserve_ansi: true,
|
|
189
|
+
preserve_control_chars: false,
|
|
190
|
+
};
|
|
191
|
+
let text = "Hello\x00World";
|
|
192
|
+
assert_eq!(config.process_output(text), "HelloWorld");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -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
|