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 cmd.arg(crate::utils::with_exported_process_context(
199 cmd_str,
200 effective_env.as_ref(),
201 ));
202
203 cmd.stdin(Stdio::piped());
205 cmd.stdout(Stdio::piped());
206 cmd.stderr(Stdio::piped());
207
208 if let Some(cwd) = crate::resolve_spawn_cwd(effective_cwd.as_ref()) {
211 cmd.current_dir(cwd);
212 }
213
214 if let Some(ref env_vars) = effective_env {
216 for (key, value) in env_vars {
217 cmd.env(key, value);
218 }
219 }
220
221 let mut child = cmd.spawn()?;
223
224 if let Some(ref stdin_content) = current_stdin {
226 if let Some(mut stdin) = child.stdin.take() {
227 let content = stdin_content.clone();
228 tokio::spawn(async move {
229 let _ = stdin.write_all(content.as_bytes()).await;
230 let _ = stdin.shutdown().await;
231 });
232 }
233 }
234
235 let mut stdout_content = String::new();
237 if let Some(mut stdout) = child.stdout.take() {
238 stdout.read_to_string(&mut stdout_content).await?;
239 }
240
241 let mut stderr_content = String::new();
243 if let Some(mut stderr) = child.stderr.take() {
244 stderr.read_to_string(&mut stderr_content).await?;
245 }
246
247 if is_last && self.mirror {
249 if !stdout_content.is_empty() {
250 print!("{}", stdout_content);
251 }
252 if !stderr_content.is_empty() {
253 eprint!("{}", stderr_content);
254 }
255 }
256
257 let status = child.wait().await?;
259 let code = status.code().unwrap_or(-1);
260
261 accumulated_stderr.push_str(&stderr_content);
262
263 if code != 0 {
264 return Ok(CommandResult {
265 stdout: stdout_content,
266 stderr: accumulated_stderr,
267 code,
268 });
269 }
270
271 current_stdin = Some(stdout_content.clone());
273 last_result = CommandResult {
274 stdout: stdout_content,
275 stderr: String::new(),
276 code,
277 };
278 }
279
280 Ok(CommandResult {
281 stdout: last_result.stdout,
282 stderr: accumulated_stderr,
283 code: last_result.code,
284 })
285 }
286
287 async fn try_virtual_command(
289 &self,
290 cmd_name: &str,
291 full_cmd: &str,
292 stdin: &Option<String>,
293 cwd: Option<&PathBuf>,
294 env: Option<&HashMap<String, String>>,
295 ) -> Option<VirtualCommandResult> {
296 let words = crate::shell_parser::split_command_words(full_cmd);
299 let args: Vec<String> = words.into_iter().skip(1).collect();
300
301 let ctx = crate::commands::CommandContext {
302 args,
303 stdin: stdin.clone(),
304 cwd: cwd.cloned(),
305 env: env.cloned(),
306 output_tx: None,
307 is_cancelled: None,
308 };
309
310 let (result, cd_context) = match cmd_name {
311 "echo" => (crate::commands::echo(ctx).await, None),
312 "pwd" => (crate::commands::pwd(ctx).await, None),
313 "cd" => crate::commands::cd::resolve_cd(ctx).await,
314 "true" => (crate::commands::r#true(ctx).await, None),
315 "false" => (crate::commands::r#false(ctx).await, None),
316 "sleep" => (crate::commands::sleep(ctx).await, None),
317 "cat" => (crate::commands::cat(ctx).await, None),
318 "ls" => (crate::commands::ls(ctx).await, None),
319 "mkdir" => (crate::commands::mkdir(ctx).await, None),
320 "rm" => (crate::commands::rm(ctx).await, None),
321 "touch" => (crate::commands::touch(ctx).await, None),
322 "cp" => (crate::commands::cp(ctx).await, None),
323 "mv" => (crate::commands::mv(ctx).await, None),
324 "basename" => (crate::commands::basename(ctx).await, None),
325 "dirname" => (crate::commands::dirname(ctx).await, None),
326 "env" => (crate::commands::env(ctx).await, None),
327 "exit" => (crate::commands::exit(ctx).await, None),
328 "which" => (crate::commands::which(ctx).await, None),
329 "yes" => (crate::commands::yes(ctx).await, None),
330 "seq" => (crate::commands::seq(ctx).await, None),
331 "test" => (crate::commands::test(ctx).await, None),
332 _ => return None,
333 };
334 Some(VirtualCommandResult { result, cd_context })
335 }
336}
337
338#[derive(Debug, Clone)]
340struct ShellConfig {
341 cmd: String,
342 args: Vec<String>,
343}
344
345fn find_available_shell() -> ShellConfig {
347 let is_windows = cfg!(windows);
348
349 if is_windows {
350 ShellConfig {
351 cmd: "cmd.exe".to_string(),
352 args: vec!["/c".to_string()],
353 }
354 } else {
355 let shells = [
356 ("/bin/sh", "-c"),
357 ("/usr/bin/sh", "-c"),
358 ("/bin/bash", "-c"),
359 ];
360
361 for (cmd, arg) in shells {
362 if std::path::Path::new(cmd).exists() {
363 return ShellConfig {
364 cmd: cmd.to_string(),
365 args: vec![arg.to_string()],
366 };
367 }
368 }
369
370 ShellConfig {
371 cmd: "/bin/sh".to_string(),
372 args: vec!["-c".to_string()],
373 }
374 }
375}
376
377pub trait PipelineExt {
379 fn pipe(self, command: impl Into<String>) -> PipelineBuilder;
381}
382
383impl PipelineExt for crate::ProcessRunner {
384 fn pipe(self, command: impl Into<String>) -> PipelineBuilder {
385 PipelineBuilder {
386 first: self,
387 additional: vec![command.into()],
388 }
389 }
390}
391
392pub struct PipelineBuilder {
394 first: crate::ProcessRunner,
395 additional: Vec<String>,
396}
397
398impl PipelineBuilder {
399 pub fn pipe(mut self, command: impl Into<String>) -> Self {
401 self.additional.push(command.into());
402 self
403 }
404
405 pub async fn run(mut self) -> Result<CommandResult> {
407 let first_result = self.first.run().await?;
409
410 if first_result.code != 0 {
411 return Ok(first_result);
412 }
413
414 let mut current_stdin = Some(first_result.stdout);
416 let mut accumulated_stderr = first_result.stderr;
417 let mut last_result = CommandResult {
418 stdout: String::new(),
419 stderr: String::new(),
420 code: 0,
421 };
422
423 for cmd_str in &self.additional {
424 let mut runner = crate::ProcessRunner::new(
425 cmd_str.clone(),
426 RunOptions {
427 stdin: StdinOption::Content(current_stdin.take().unwrap_or_default()),
428 mirror: false,
429 capture: true,
430 ..Default::default()
431 },
432 );
433
434 let result = runner.run().await?;
435 accumulated_stderr.push_str(&result.stderr);
436
437 if result.code != 0 {
438 return Ok(CommandResult {
439 stdout: result.stdout,
440 stderr: accumulated_stderr,
441 code: result.code,
442 });
443 }
444
445 current_stdin = Some(result.stdout.clone());
446 last_result = result;
447 }
448
449 Ok(CommandResult {
450 stdout: last_result.stdout,
451 stderr: accumulated_stderr,
452 code: last_result.code,
453 })
454 }
455}