1use std::collections::HashMap;
28use std::path::PathBuf;
29use std::process::Stdio;
30use tokio::io::{AsyncReadExt, AsyncWriteExt};
31use tokio::process::Command;
32
33use crate::trace::trace_lazy;
34use crate::{CommandResult, Result, RunOptions, StdinOption};
35
36struct VirtualCommandResult {
37 result: CommandResult,
38 cd_context: Option<crate::commands::cd::CdContext>,
39}
40
41#[derive(Debug, Clone)]
45pub struct Pipeline {
46 commands: Vec<String>,
48 stdin: Option<String>,
50 cwd: Option<PathBuf>,
52 env: Option<HashMap<String, String>>,
54 mirror: bool,
56 capture: bool,
58}
59
60impl Default for Pipeline {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl Pipeline {
67 pub fn new() -> Self {
69 Pipeline {
70 commands: Vec::new(),
71 stdin: None,
72 cwd: None,
73 env: None,
74 mirror: true,
75 capture: true,
76 }
77 }
78
79 pub fn add(mut self, command: impl Into<String>) -> Self {
81 self.commands.push(command.into());
82 self
83 }
84
85 pub fn stdin(mut self, content: impl Into<String>) -> Self {
87 self.stdin = Some(content.into());
88 self
89 }
90
91 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
93 self.cwd = Some(path.into());
94 self
95 }
96
97 pub fn env(mut self, env: HashMap<String, String>) -> Self {
99 self.env = Some(env);
100 self
101 }
102
103 pub fn mirror_output(mut self, mirror: bool) -> Self {
105 self.mirror = mirror;
106 self
107 }
108
109 pub fn capture_output(mut self, capture: bool) -> Self {
111 self.capture = capture;
112 self
113 }
114
115 pub async fn run(self) -> Result<CommandResult> {
117 if self.commands.is_empty() {
118 return Ok(CommandResult {
119 stdout: String::new(),
120 stderr: "No commands in pipeline".to_string(),
121 code: 1,
122 });
123 }
124
125 trace_lazy("Pipeline", || {
126 format!("Running pipeline with {} commands", self.commands.len())
127 });
128
129 let mut current_stdin = self.stdin.clone();
130 let mut effective_cwd = self.cwd.clone();
131 let mut effective_env = self.env.clone();
132 let mut last_result = CommandResult {
133 stdout: String::new(),
134 stderr: String::new(),
135 code: 0,
136 };
137 let mut accumulated_stderr = String::new();
138
139 for (i, cmd_str) in self.commands.iter().enumerate() {
140 let is_last = i == self.commands.len() - 1;
141
142 trace_lazy("Pipeline", || {
143 format!(
144 "Executing command {}/{}: {}",
145 i + 1,
146 self.commands.len(),
147 cmd_str
148 )
149 });
150
151 let first_word = cmd_str.split_whitespace().next().unwrap_or("");
153 if crate::commands::are_virtual_commands_enabled() {
154 if let Some(result) = self
155 .try_virtual_command(
156 first_word,
157 cmd_str,
158 ¤t_stdin,
159 effective_cwd.as_ref(),
160 effective_env.as_ref(),
161 )
162 .await
163 {
164 let VirtualCommandResult { result, cd_context } = result;
165 if result.code != 0 {
166 return Ok(CommandResult {
167 stdout: result.stdout,
168 stderr: accumulated_stderr + &result.stderr,
169 code: result.code,
170 });
171 }
172 current_stdin = Some(result.stdout.clone());
173 accumulated_stderr.push_str(&result.stderr);
174 if let Some(context) = cd_context {
175 let env = effective_env.get_or_insert_with(|| std::env::vars().collect());
176 env.insert(
177 "OLDPWD".to_string(),
178 context.oldpwd.to_string_lossy().to_string(),
179 );
180 env.insert("PWD".to_string(), context.cwd.to_string_lossy().to_string());
181 effective_cwd = Some(context.cwd);
182 }
183 last_result = result;
184 continue;
185 }
186 }
187
188 let shell = find_available_shell();
190 let mut cmd = Command::new(&shell.cmd);
191 for arg in &shell.args {
192 cmd.arg(arg);
193 }
194 cmd.arg(crate::utils::with_exported_process_context(
195 cmd_str,
196 effective_env.as_ref(),
197 ));
198
199 cmd.stdin(Stdio::piped());
201 cmd.stdout(Stdio::piped());
202 cmd.stderr(Stdio::piped());
203
204 if let Some(cwd) = crate::resolve_spawn_cwd(effective_cwd.as_ref()) {
207 cmd.current_dir(cwd);
208 }
209
210 if let Some(ref env_vars) = effective_env {
212 for (key, value) in env_vars {
213 cmd.env(key, value);
214 }
215 }
216
217 let mut child = cmd.spawn()?;
219
220 if let Some(ref stdin_content) = current_stdin {
222 if let Some(mut stdin) = child.stdin.take() {
223 let content = stdin_content.clone();
224 tokio::spawn(async move {
225 let _ = stdin.write_all(content.as_bytes()).await;
226 let _ = stdin.shutdown().await;
227 });
228 }
229 }
230
231 let mut stdout_content = String::new();
233 if let Some(mut stdout) = child.stdout.take() {
234 stdout.read_to_string(&mut stdout_content).await?;
235 }
236
237 let mut stderr_content = String::new();
239 if let Some(mut stderr) = child.stderr.take() {
240 stderr.read_to_string(&mut stderr_content).await?;
241 }
242
243 if is_last && self.mirror {
245 if !stdout_content.is_empty() {
246 print!("{}", stdout_content);
247 }
248 if !stderr_content.is_empty() {
249 eprint!("{}", stderr_content);
250 }
251 }
252
253 let status = child.wait().await?;
255 let code = status.code().unwrap_or(-1);
256
257 accumulated_stderr.push_str(&stderr_content);
258
259 if code != 0 {
260 return Ok(CommandResult {
261 stdout: stdout_content,
262 stderr: accumulated_stderr,
263 code,
264 });
265 }
266
267 current_stdin = Some(stdout_content.clone());
269 last_result = CommandResult {
270 stdout: stdout_content,
271 stderr: String::new(),
272 code,
273 };
274 }
275
276 Ok(CommandResult {
277 stdout: last_result.stdout,
278 stderr: accumulated_stderr,
279 code: last_result.code,
280 })
281 }
282
283 async fn try_virtual_command(
285 &self,
286 cmd_name: &str,
287 full_cmd: &str,
288 stdin: &Option<String>,
289 cwd: Option<&PathBuf>,
290 env: Option<&HashMap<String, String>>,
291 ) -> Option<VirtualCommandResult> {
292 let parts: Vec<&str> = full_cmd.split_whitespace().collect();
293 let args: Vec<String> = parts.iter().skip(1).map(|s| s.to_string()).collect();
294
295 let ctx = crate::commands::CommandContext {
296 args,
297 stdin: stdin.clone(),
298 cwd: cwd.cloned(),
299 env: env.cloned(),
300 output_tx: None,
301 is_cancelled: None,
302 };
303
304 let (result, cd_context) = match cmd_name {
305 "echo" => (crate::commands::echo(ctx).await, None),
306 "pwd" => (crate::commands::pwd(ctx).await, None),
307 "cd" => crate::commands::cd::resolve_cd(ctx).await,
308 "true" => (crate::commands::r#true(ctx).await, None),
309 "false" => (crate::commands::r#false(ctx).await, None),
310 "sleep" => (crate::commands::sleep(ctx).await, None),
311 "cat" => (crate::commands::cat(ctx).await, None),
312 "ls" => (crate::commands::ls(ctx).await, None),
313 "mkdir" => (crate::commands::mkdir(ctx).await, None),
314 "rm" => (crate::commands::rm(ctx).await, None),
315 "touch" => (crate::commands::touch(ctx).await, None),
316 "cp" => (crate::commands::cp(ctx).await, None),
317 "mv" => (crate::commands::mv(ctx).await, None),
318 "basename" => (crate::commands::basename(ctx).await, None),
319 "dirname" => (crate::commands::dirname(ctx).await, None),
320 "env" => (crate::commands::env(ctx).await, None),
321 "exit" => (crate::commands::exit(ctx).await, None),
322 "which" => (crate::commands::which(ctx).await, None),
323 "yes" => (crate::commands::yes(ctx).await, None),
324 "seq" => (crate::commands::seq(ctx).await, None),
325 "test" => (crate::commands::test(ctx).await, None),
326 _ => return None,
327 };
328 Some(VirtualCommandResult { result, cd_context })
329 }
330}
331
332#[derive(Debug, Clone)]
334struct ShellConfig {
335 cmd: String,
336 args: Vec<String>,
337}
338
339fn find_available_shell() -> ShellConfig {
341 let is_windows = cfg!(windows);
342
343 if is_windows {
344 ShellConfig {
345 cmd: "cmd.exe".to_string(),
346 args: vec!["/c".to_string()],
347 }
348 } else {
349 let shells = [
350 ("/bin/sh", "-c"),
351 ("/usr/bin/sh", "-c"),
352 ("/bin/bash", "-c"),
353 ];
354
355 for (cmd, arg) in shells {
356 if std::path::Path::new(cmd).exists() {
357 return ShellConfig {
358 cmd: cmd.to_string(),
359 args: vec![arg.to_string()],
360 };
361 }
362 }
363
364 ShellConfig {
365 cmd: "/bin/sh".to_string(),
366 args: vec!["-c".to_string()],
367 }
368 }
369}
370
371pub trait PipelineExt {
373 fn pipe(self, command: impl Into<String>) -> PipelineBuilder;
375}
376
377impl PipelineExt for crate::ProcessRunner {
378 fn pipe(self, command: impl Into<String>) -> PipelineBuilder {
379 PipelineBuilder {
380 first: self,
381 additional: vec![command.into()],
382 }
383 }
384}
385
386pub struct PipelineBuilder {
388 first: crate::ProcessRunner,
389 additional: Vec<String>,
390}
391
392impl PipelineBuilder {
393 pub fn pipe(mut self, command: impl Into<String>) -> Self {
395 self.additional.push(command.into());
396 self
397 }
398
399 pub async fn run(mut self) -> Result<CommandResult> {
401 let first_result = self.first.run().await?;
403
404 if first_result.code != 0 {
405 return Ok(first_result);
406 }
407
408 let mut current_stdin = Some(first_result.stdout);
410 let mut accumulated_stderr = first_result.stderr;
411 let mut last_result = CommandResult {
412 stdout: String::new(),
413 stderr: String::new(),
414 code: 0,
415 };
416
417 for cmd_str in &self.additional {
418 let mut runner = crate::ProcessRunner::new(
419 cmd_str.clone(),
420 RunOptions {
421 stdin: StdinOption::Content(current_stdin.take().unwrap_or_default()),
422 mirror: false,
423 capture: true,
424 ..Default::default()
425 },
426 );
427
428 let result = runner.run().await?;
429 accumulated_stderr.push_str(&result.stderr);
430
431 if result.code != 0 {
432 return Ok(CommandResult {
433 stdout: result.stdout,
434 stderr: accumulated_stderr,
435 code: result.code,
436 });
437 }
438
439 current_stdin = Some(result.stdout.clone());
440 last_result = result;
441 }
442
443 Ok(CommandResult {
444 stdout: last_result.stdout,
445 stderr: accumulated_stderr,
446 code: last_result.code,
447 })
448 }
449}