1use std::collections::HashMap;
28use std::path::PathBuf;
29use std::process::Stdio;
30use tokio::io::{AsyncReadExt, AsyncWriteExt};
31
32use crate::trace::trace_lazy;
33use crate::{CommandResult, Result, RunOptions, StdinOption};
34
35struct VirtualCommandResult {
36 result: CommandResult,
37 cd_context: Option<crate::commands::cd::CdContext>,
38}
39
40fn pipeline_exit_code(exit_codes: &[i32], pipefail: bool) -> i32 {
41 let last = exit_codes.last().copied().unwrap_or(0);
42 if pipefail {
43 exit_codes
44 .iter()
45 .rev()
46 .copied()
47 .find(|code| *code != 0)
48 .unwrap_or(last)
49 } else {
50 last
51 }
52}
53
54#[derive(Debug, Clone)]
58pub struct Pipeline {
59 commands: Vec<String>,
61 stdin: Option<String>,
63 cwd: Option<PathBuf>,
65 env: Option<HashMap<String, String>>,
67 mirror: bool,
69 capture: bool,
71}
72
73impl Default for Pipeline {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl Pipeline {
80 pub fn new() -> Self {
82 Pipeline {
83 commands: Vec::new(),
84 stdin: None,
85 cwd: None,
86 env: None,
87 mirror: true,
88 capture: true,
89 }
90 }
91
92 #[allow(clippy::should_implement_trait)]
97 pub fn add(mut self, command: impl Into<String>) -> Self {
98 self.commands.push(command.into());
99 self
100 }
101
102 pub fn stdin(mut self, content: impl Into<String>) -> Self {
104 self.stdin = Some(content.into());
105 self
106 }
107
108 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
110 self.cwd = Some(path.into());
111 self
112 }
113
114 pub fn env(mut self, env: HashMap<String, String>) -> Self {
116 self.env = Some(env);
117 self
118 }
119
120 pub fn mirror_output(mut self, mirror: bool) -> Self {
122 self.mirror = mirror;
123 self
124 }
125
126 pub fn capture_output(mut self, capture: bool) -> Self {
128 self.capture = capture;
129 self
130 }
131
132 pub async fn run(self) -> Result<CommandResult> {
134 if self.commands.is_empty() {
135 return Ok(CommandResult {
136 stdout: String::new(),
137 stderr: "No commands in pipeline".to_string(),
138 code: 1,
139 });
140 }
141
142 trace_lazy("Pipeline", || {
143 format!("Running pipeline with {} commands", self.commands.len())
144 });
145
146 let mut current_stdin = self.stdin.clone();
147 let mut effective_cwd = self.cwd.clone();
148 let mut effective_env = self.env.clone();
149 let mut last_result = CommandResult {
150 stdout: String::new(),
151 stderr: String::new(),
152 code: 0,
153 };
154 let mut accumulated_stderr = String::new();
155 let mut exit_codes = Vec::with_capacity(self.commands.len());
156 let pipefail = crate::get_shell_settings().await.pipefail;
157
158 for (i, cmd_str) in self.commands.iter().enumerate() {
159 let is_last = i == self.commands.len() - 1;
160
161 trace_lazy("Pipeline", || {
162 format!(
163 "Executing command {}/{}: {}",
164 i + 1,
165 self.commands.len(),
166 cmd_str
167 )
168 });
169
170 let first_word = cmd_str.split_whitespace().next().unwrap_or("");
172 if crate::commands::are_virtual_commands_enabled() {
173 if let Some(result) = self
174 .try_virtual_command(
175 first_word,
176 cmd_str,
177 ¤t_stdin,
178 effective_cwd.as_ref(),
179 effective_env.as_ref(),
180 )
181 .await
182 {
183 let VirtualCommandResult { result, cd_context } = result;
184 exit_codes.push(result.code);
185 current_stdin = Some(result.stdout.clone());
186 accumulated_stderr.push_str(&result.stderr);
187 if result.code == 0 {
188 if let Some(context) = cd_context {
189 let env =
190 effective_env.get_or_insert_with(|| std::env::vars().collect());
191 env.insert(
192 "OLDPWD".to_string(),
193 context.oldpwd.to_string_lossy().to_string(),
194 );
195 env.insert(
196 "PWD".to_string(),
197 context.cwd.to_string_lossy().to_string(),
198 );
199 effective_cwd = Some(context.cwd);
200 }
201 }
202 last_result = result;
203 continue;
204 }
205 }
206
207 let mut cmd = crate::utils::shell_command(cmd_str, effective_env.as_ref());
209
210 cmd.stdin(Stdio::piped());
212 cmd.stdout(Stdio::piped());
213 cmd.stderr(Stdio::piped());
214
215 if let Some(cwd) = crate::resolve_spawn_cwd(effective_cwd.as_ref()) {
218 cmd.current_dir(cwd);
219 }
220
221 if let Some(ref env_vars) = effective_env {
223 for (key, value) in env_vars {
224 cmd.env(key, value);
225 }
226 }
227
228 let mut child = cmd.spawn()?;
230
231 if let Some(ref stdin_content) = current_stdin {
233 if let Some(mut stdin) = child.stdin.take() {
234 let content = stdin_content.clone();
235 tokio::spawn(async move {
236 let _ = stdin.write_all(content.as_bytes()).await;
237 let _ = stdin.shutdown().await;
238 });
239 }
240 }
241
242 let mut stdout_content = String::new();
244 if let Some(mut stdout) = child.stdout.take() {
245 stdout.read_to_string(&mut stdout_content).await?;
246 }
247
248 let mut stderr_content = String::new();
250 if let Some(mut stderr) = child.stderr.take() {
251 stderr.read_to_string(&mut stderr_content).await?;
252 }
253
254 if is_last && self.mirror {
256 if !stdout_content.is_empty() {
257 print!("{}", stdout_content);
258 }
259 if !stderr_content.is_empty() {
260 eprint!("{}", stderr_content);
261 }
262 }
263
264 let status = child.wait().await?;
266 let code = status.code().unwrap_or(-1);
267
268 accumulated_stderr.push_str(&stderr_content);
269 exit_codes.push(code);
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: pipeline_exit_code(&exit_codes, pipefail),
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 "tee" => (crate::commands::tee(ctx).await, None),
332 "test" => (crate::commands::test(ctx).await, None),
333 _ => return None,
334 };
335 Some(VirtualCommandResult { result, cd_context })
336 }
337}
338
339pub trait PipelineExt {
341 fn pipe(self, command: impl Into<String>) -> PipelineBuilder;
343}
344
345impl PipelineExt for crate::ProcessRunner {
346 fn pipe(self, command: impl Into<String>) -> PipelineBuilder {
347 PipelineBuilder {
348 first: self,
349 additional: vec![command.into()],
350 }
351 }
352}
353
354pub struct PipelineBuilder {
356 first: crate::ProcessRunner,
357 additional: Vec<String>,
358}
359
360impl PipelineBuilder {
361 pub fn pipe(mut self, command: impl Into<String>) -> Self {
363 self.additional.push(command.into());
364 self
365 }
366
367 pub async fn run(mut self) -> Result<CommandResult> {
369 let first_result = self.first.run().await?;
371 let pipefail = crate::get_shell_settings().await.pipefail;
372 let mut exit_codes = vec![first_result.code];
373
374 let mut current_stdin = Some(first_result.stdout.clone());
376 let mut accumulated_stderr = first_result.stderr.clone();
377 let mut last_result = first_result;
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 exit_codes.push(result.code);
393
394 current_stdin = Some(result.stdout.clone());
395 last_result = result;
396 }
397
398 Ok(CommandResult {
399 stdout: last_result.stdout,
400 stderr: accumulated_stderr,
401 code: pipeline_exit_code(&exit_codes, pipefail),
402 })
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::pipeline_exit_code;
409
410 #[test]
411 fn pipeline_status_uses_last_stage_by_default() {
412 assert_eq!(pipeline_exit_code(&[3, 0], false), 0);
413 }
414
415 #[test]
416 fn pipefail_uses_rightmost_failing_stage() {
417 assert_eq!(pipeline_exit_code(&[2, 7, 0], true), 7);
418 }
419}