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