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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
//! Pipeline execution support
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides pipeline functionality similar to the JavaScript
|
|
4
|
+
//! `$.process-runner-pipeline.mjs` module. It allows chaining commands
|
|
5
|
+
//! together with the output of one command becoming the input of the next.
|
|
6
|
+
//!
|
|
7
|
+
//! ## Usage
|
|
8
|
+
//!
|
|
9
|
+
//! ```rust,no_run
|
|
10
|
+
//! use command_stream::{Pipeline, run};
|
|
11
|
+
//!
|
|
12
|
+
//! #[tokio::main]
|
|
13
|
+
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
14
|
+
//! // Create a pipeline
|
|
15
|
+
//! let result = Pipeline::new()
|
|
16
|
+
//! .add("echo hello world")
|
|
17
|
+
//! .add("grep world")
|
|
18
|
+
//! .add("wc -l")
|
|
19
|
+
//! .run()
|
|
20
|
+
//! .await?;
|
|
21
|
+
//!
|
|
22
|
+
//! println!("Output: {}", result.stdout);
|
|
23
|
+
//! Ok(())
|
|
24
|
+
//! }
|
|
25
|
+
//! ```
|
|
26
|
+
|
|
27
|
+
use std::collections::HashMap;
|
|
28
|
+
use std::path::PathBuf;
|
|
29
|
+
use std::process::Stdio;
|
|
30
|
+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
31
|
+
use tokio::process::Command;
|
|
32
|
+
|
|
33
|
+
use crate::trace::trace_lazy;
|
|
34
|
+
use crate::{CommandResult, Result, RunOptions, StdinOption};
|
|
35
|
+
|
|
36
|
+
/// A pipeline of commands to be executed sequentially
|
|
37
|
+
///
|
|
38
|
+
/// Each command's stdout is piped to the next command's stdin.
|
|
39
|
+
#[derive(Debug, Clone)]
|
|
40
|
+
pub struct Pipeline {
|
|
41
|
+
/// Commands in the pipeline
|
|
42
|
+
commands: Vec<String>,
|
|
43
|
+
/// Initial stdin content (optional)
|
|
44
|
+
stdin: Option<String>,
|
|
45
|
+
/// Working directory
|
|
46
|
+
cwd: Option<PathBuf>,
|
|
47
|
+
/// Environment variables
|
|
48
|
+
env: Option<HashMap<String, String>>,
|
|
49
|
+
/// Whether to mirror output to parent stdout/stderr
|
|
50
|
+
mirror: bool,
|
|
51
|
+
/// Whether to capture output
|
|
52
|
+
capture: bool,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
impl Default for Pipeline {
|
|
56
|
+
fn default() -> Self {
|
|
57
|
+
Self::new()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl Pipeline {
|
|
62
|
+
/// Create a new empty pipeline
|
|
63
|
+
pub fn new() -> Self {
|
|
64
|
+
Pipeline {
|
|
65
|
+
commands: Vec::new(),
|
|
66
|
+
stdin: None,
|
|
67
|
+
cwd: None,
|
|
68
|
+
env: None,
|
|
69
|
+
mirror: true,
|
|
70
|
+
capture: true,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Add a command to the pipeline
|
|
75
|
+
pub fn add(mut self, command: impl Into<String>) -> Self {
|
|
76
|
+
self.commands.push(command.into());
|
|
77
|
+
self
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Set the initial stdin content for the first command
|
|
81
|
+
pub fn stdin(mut self, content: impl Into<String>) -> Self {
|
|
82
|
+
self.stdin = Some(content.into());
|
|
83
|
+
self
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Set the working directory for all commands
|
|
87
|
+
pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
|
|
88
|
+
self.cwd = Some(path.into());
|
|
89
|
+
self
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Set environment variables for all commands
|
|
93
|
+
pub fn env(mut self, env: HashMap<String, String>) -> Self {
|
|
94
|
+
self.env = Some(env);
|
|
95
|
+
self
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Set whether to mirror output to stdout/stderr
|
|
99
|
+
pub fn mirror_output(mut self, mirror: bool) -> Self {
|
|
100
|
+
self.mirror = mirror;
|
|
101
|
+
self
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Set whether to capture output
|
|
105
|
+
pub fn capture_output(mut self, capture: bool) -> Self {
|
|
106
|
+
self.capture = capture;
|
|
107
|
+
self
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Execute the pipeline and return the result
|
|
111
|
+
pub async fn run(self) -> Result<CommandResult> {
|
|
112
|
+
if self.commands.is_empty() {
|
|
113
|
+
return Ok(CommandResult {
|
|
114
|
+
stdout: String::new(),
|
|
115
|
+
stderr: "No commands in pipeline".to_string(),
|
|
116
|
+
code: 1,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
trace_lazy("Pipeline", || {
|
|
121
|
+
format!("Running pipeline with {} commands", self.commands.len())
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let mut current_stdin = self.stdin.clone();
|
|
125
|
+
let mut last_result = CommandResult {
|
|
126
|
+
stdout: String::new(),
|
|
127
|
+
stderr: String::new(),
|
|
128
|
+
code: 0,
|
|
129
|
+
};
|
|
130
|
+
let mut accumulated_stderr = String::new();
|
|
131
|
+
|
|
132
|
+
for (i, cmd_str) in self.commands.iter().enumerate() {
|
|
133
|
+
let is_last = i == self.commands.len() - 1;
|
|
134
|
+
|
|
135
|
+
trace_lazy("Pipeline", || {
|
|
136
|
+
format!("Executing command {}/{}: {}", i + 1, self.commands.len(), cmd_str)
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Check if this is a virtual command
|
|
140
|
+
let first_word = cmd_str.split_whitespace().next().unwrap_or("");
|
|
141
|
+
if crate::commands::are_virtual_commands_enabled() {
|
|
142
|
+
if let Some(result) = self.try_virtual_command(first_word, cmd_str, ¤t_stdin).await {
|
|
143
|
+
if result.code != 0 {
|
|
144
|
+
return Ok(CommandResult {
|
|
145
|
+
stdout: result.stdout,
|
|
146
|
+
stderr: accumulated_stderr + &result.stderr,
|
|
147
|
+
code: result.code,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
current_stdin = Some(result.stdout.clone());
|
|
151
|
+
accumulated_stderr.push_str(&result.stderr);
|
|
152
|
+
last_result = result;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Execute via shell
|
|
158
|
+
let shell = find_available_shell();
|
|
159
|
+
let mut cmd = Command::new(&shell.cmd);
|
|
160
|
+
for arg in &shell.args {
|
|
161
|
+
cmd.arg(arg);
|
|
162
|
+
}
|
|
163
|
+
cmd.arg(cmd_str);
|
|
164
|
+
|
|
165
|
+
// Configure stdio
|
|
166
|
+
cmd.stdin(Stdio::piped());
|
|
167
|
+
cmd.stdout(Stdio::piped());
|
|
168
|
+
cmd.stderr(Stdio::piped());
|
|
169
|
+
|
|
170
|
+
// Set working directory
|
|
171
|
+
if let Some(ref cwd) = self.cwd {
|
|
172
|
+
cmd.current_dir(cwd);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Set environment
|
|
176
|
+
if let Some(ref env_vars) = self.env {
|
|
177
|
+
for (key, value) in env_vars {
|
|
178
|
+
cmd.env(key, value);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Spawn the process
|
|
183
|
+
let mut child = cmd.spawn()?;
|
|
184
|
+
|
|
185
|
+
// Write stdin if available
|
|
186
|
+
if let Some(ref stdin_content) = current_stdin {
|
|
187
|
+
if let Some(mut stdin) = child.stdin.take() {
|
|
188
|
+
let content = stdin_content.clone();
|
|
189
|
+
tokio::spawn(async move {
|
|
190
|
+
let _ = stdin.write_all(content.as_bytes()).await;
|
|
191
|
+
let _ = stdin.shutdown().await;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Read stdout
|
|
197
|
+
let mut stdout_content = String::new();
|
|
198
|
+
if let Some(mut stdout) = child.stdout.take() {
|
|
199
|
+
stdout.read_to_string(&mut stdout_content).await?;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Read stderr
|
|
203
|
+
let mut stderr_content = String::new();
|
|
204
|
+
if let Some(mut stderr) = child.stderr.take() {
|
|
205
|
+
stderr.read_to_string(&mut stderr_content).await?;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Mirror output if enabled and this is the last command
|
|
209
|
+
if is_last && self.mirror {
|
|
210
|
+
if !stdout_content.is_empty() {
|
|
211
|
+
print!("{}", stdout_content);
|
|
212
|
+
}
|
|
213
|
+
if !stderr_content.is_empty() {
|
|
214
|
+
eprint!("{}", stderr_content);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Wait for the process
|
|
219
|
+
let status = child.wait().await?;
|
|
220
|
+
let code = status.code().unwrap_or(-1);
|
|
221
|
+
|
|
222
|
+
accumulated_stderr.push_str(&stderr_content);
|
|
223
|
+
|
|
224
|
+
if code != 0 {
|
|
225
|
+
return Ok(CommandResult {
|
|
226
|
+
stdout: stdout_content,
|
|
227
|
+
stderr: accumulated_stderr,
|
|
228
|
+
code,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Set up stdin for next command
|
|
233
|
+
current_stdin = Some(stdout_content.clone());
|
|
234
|
+
last_result = CommandResult {
|
|
235
|
+
stdout: stdout_content,
|
|
236
|
+
stderr: String::new(),
|
|
237
|
+
code,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
Ok(CommandResult {
|
|
242
|
+
stdout: last_result.stdout,
|
|
243
|
+
stderr: accumulated_stderr,
|
|
244
|
+
code: last_result.code,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// Try to execute a virtual command
|
|
249
|
+
async fn try_virtual_command(
|
|
250
|
+
&self,
|
|
251
|
+
cmd_name: &str,
|
|
252
|
+
full_cmd: &str,
|
|
253
|
+
stdin: &Option<String>,
|
|
254
|
+
) -> Option<CommandResult> {
|
|
255
|
+
let parts: Vec<&str> = full_cmd.split_whitespace().collect();
|
|
256
|
+
let args: Vec<String> = parts.iter().skip(1).map(|s| s.to_string()).collect();
|
|
257
|
+
|
|
258
|
+
let ctx = crate::commands::CommandContext {
|
|
259
|
+
args,
|
|
260
|
+
stdin: stdin.clone(),
|
|
261
|
+
cwd: self.cwd.clone(),
|
|
262
|
+
env: self.env.clone(),
|
|
263
|
+
output_tx: None,
|
|
264
|
+
is_cancelled: None,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
match cmd_name {
|
|
268
|
+
"echo" => Some(crate::commands::echo(ctx).await),
|
|
269
|
+
"pwd" => Some(crate::commands::pwd(ctx).await),
|
|
270
|
+
"cd" => Some(crate::commands::cd(ctx).await),
|
|
271
|
+
"true" => Some(crate::commands::r#true(ctx).await),
|
|
272
|
+
"false" => Some(crate::commands::r#false(ctx).await),
|
|
273
|
+
"sleep" => Some(crate::commands::sleep(ctx).await),
|
|
274
|
+
"cat" => Some(crate::commands::cat(ctx).await),
|
|
275
|
+
"ls" => Some(crate::commands::ls(ctx).await),
|
|
276
|
+
"mkdir" => Some(crate::commands::mkdir(ctx).await),
|
|
277
|
+
"rm" => Some(crate::commands::rm(ctx).await),
|
|
278
|
+
"touch" => Some(crate::commands::touch(ctx).await),
|
|
279
|
+
"cp" => Some(crate::commands::cp(ctx).await),
|
|
280
|
+
"mv" => Some(crate::commands::mv(ctx).await),
|
|
281
|
+
"basename" => Some(crate::commands::basename(ctx).await),
|
|
282
|
+
"dirname" => Some(crate::commands::dirname(ctx).await),
|
|
283
|
+
"env" => Some(crate::commands::env(ctx).await),
|
|
284
|
+
"exit" => Some(crate::commands::exit(ctx).await),
|
|
285
|
+
"which" => Some(crate::commands::which(ctx).await),
|
|
286
|
+
"yes" => Some(crate::commands::yes(ctx).await),
|
|
287
|
+
"seq" => Some(crate::commands::seq(ctx).await),
|
|
288
|
+
"test" => Some(crate::commands::test(ctx).await),
|
|
289
|
+
_ => None,
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// Shell configuration
|
|
295
|
+
#[derive(Debug, Clone)]
|
|
296
|
+
struct ShellConfig {
|
|
297
|
+
cmd: String,
|
|
298
|
+
args: Vec<String>,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Find an available shell
|
|
302
|
+
fn find_available_shell() -> ShellConfig {
|
|
303
|
+
let is_windows = cfg!(windows);
|
|
304
|
+
|
|
305
|
+
if is_windows {
|
|
306
|
+
ShellConfig {
|
|
307
|
+
cmd: "cmd.exe".to_string(),
|
|
308
|
+
args: vec!["/c".to_string()],
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
let shells = [
|
|
312
|
+
("/bin/sh", "-c"),
|
|
313
|
+
("/usr/bin/sh", "-c"),
|
|
314
|
+
("/bin/bash", "-c"),
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
for (cmd, arg) in shells {
|
|
318
|
+
if std::path::Path::new(cmd).exists() {
|
|
319
|
+
return ShellConfig {
|
|
320
|
+
cmd: cmd.to_string(),
|
|
321
|
+
args: vec![arg.to_string()],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
ShellConfig {
|
|
327
|
+
cmd: "/bin/sh".to_string(),
|
|
328
|
+
args: vec!["-c".to_string()],
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Extension trait to add `.pipe()` method to ProcessRunner
|
|
334
|
+
pub trait PipelineExt {
|
|
335
|
+
/// Pipe the output of this command to another command
|
|
336
|
+
fn pipe(self, command: impl Into<String>) -> PipelineBuilder;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
impl PipelineExt for crate::ProcessRunner {
|
|
340
|
+
fn pipe(self, command: impl Into<String>) -> PipelineBuilder {
|
|
341
|
+
PipelineBuilder {
|
|
342
|
+
first: self,
|
|
343
|
+
additional: vec![command.into()],
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/// Builder for piping commands together
|
|
349
|
+
pub struct PipelineBuilder {
|
|
350
|
+
first: crate::ProcessRunner,
|
|
351
|
+
additional: Vec<String>,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
impl PipelineBuilder {
|
|
355
|
+
/// Add another command to the pipeline
|
|
356
|
+
pub fn pipe(mut self, command: impl Into<String>) -> Self {
|
|
357
|
+
self.additional.push(command.into());
|
|
358
|
+
self
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Execute the pipeline
|
|
362
|
+
pub async fn run(mut self) -> Result<CommandResult> {
|
|
363
|
+
// First, run the initial command
|
|
364
|
+
let first_result = self.first.run().await?;
|
|
365
|
+
|
|
366
|
+
if first_result.code != 0 {
|
|
367
|
+
return Ok(first_result);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Then run the rest as a pipeline
|
|
371
|
+
let mut current_stdin = Some(first_result.stdout);
|
|
372
|
+
let mut accumulated_stderr = first_result.stderr;
|
|
373
|
+
let mut last_result = CommandResult {
|
|
374
|
+
stdout: String::new(),
|
|
375
|
+
stderr: String::new(),
|
|
376
|
+
code: 0,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
for cmd_str in &self.additional {
|
|
380
|
+
let mut runner = crate::ProcessRunner::new(
|
|
381
|
+
cmd_str.clone(),
|
|
382
|
+
RunOptions {
|
|
383
|
+
stdin: StdinOption::Content(current_stdin.take().unwrap_or_default()),
|
|
384
|
+
mirror: false,
|
|
385
|
+
capture: true,
|
|
386
|
+
..Default::default()
|
|
387
|
+
},
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
let result = runner.run().await?;
|
|
391
|
+
accumulated_stderr.push_str(&result.stderr);
|
|
392
|
+
|
|
393
|
+
if result.code != 0 {
|
|
394
|
+
return Ok(CommandResult {
|
|
395
|
+
stdout: result.stdout,
|
|
396
|
+
stderr: accumulated_stderr,
|
|
397
|
+
code: result.code,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
current_stdin = Some(result.stdout.clone());
|
|
402
|
+
last_result = result;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
Ok(CommandResult {
|
|
406
|
+
stdout: last_result.stdout,
|
|
407
|
+
stderr: accumulated_stderr,
|
|
408
|
+
code: last_result.code,
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
}
|