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::new("", "No commands in pipeline", 1));
136 }
137
138 trace_lazy("Pipeline", || {
139 format!("Running pipeline with {} commands", self.commands.len())
140 });
141
142 let mut current_stdin = self.stdin.clone();
143 let mut effective_cwd = self.cwd.clone();
144 let mut effective_env = self.env.clone();
145 let mut last_result = CommandResult::new("", "", 0);
146 let mut accumulated_stderr = String::new();
147 let mut exit_codes = Vec::with_capacity(self.commands.len());
148 let pipefail = crate::get_shell_settings().await.pipefail;
149
150 for (i, cmd_str) in self.commands.iter().enumerate() {
151 let is_last = i == self.commands.len() - 1;
152
153 trace_lazy("Pipeline", || {
154 format!(
155 "Executing command {}/{}: {}",
156 i + 1,
157 self.commands.len(),
158 cmd_str
159 )
160 });
161
162 let first_word = cmd_str.split_whitespace().next().unwrap_or("");
164 if crate::commands::are_virtual_commands_enabled() {
165 if let Some(result) = self
166 .try_virtual_command(
167 first_word,
168 cmd_str,
169 ¤t_stdin,
170 effective_cwd.as_ref(),
171 effective_env.as_ref(),
172 )
173 .await
174 {
175 let VirtualCommandResult { result, cd_context } = result;
176 exit_codes.push(result.code);
177 current_stdin = Some(result.stdout.to_string());
178 accumulated_stderr.push_str(&result.stderr);
179 if result.code == 0 {
180 if let Some(context) = cd_context {
181 let env =
182 effective_env.get_or_insert_with(|| std::env::vars().collect());
183 env.insert(
184 "OLDPWD".to_string(),
185 context.oldpwd.to_string_lossy().to_string(),
186 );
187 env.insert(
188 "PWD".to_string(),
189 context.cwd.to_string_lossy().to_string(),
190 );
191 effective_cwd = Some(context.cwd);
192 }
193 }
194 last_result = result;
195 continue;
196 }
197 }
198
199 let mut cmd = crate::utils::shell_command(cmd_str, effective_env.as_ref());
201
202 cmd.stdin(Stdio::piped());
204 cmd.stdout(Stdio::piped());
205 cmd.stderr(Stdio::piped());
206
207 if let Some(cwd) = crate::resolve_spawn_cwd(effective_cwd.as_ref()) {
210 cmd.current_dir(cwd);
211 }
212
213 if let Some(ref env_vars) = effective_env {
215 for (key, value) in env_vars {
216 cmd.env(key, value);
217 }
218 }
219
220 let mut child = cmd.spawn()?;
222
223 if let Some(ref stdin_content) = current_stdin {
225 if let Some(mut stdin) = child.stdin.take() {
226 let content = stdin_content.clone();
227 tokio::spawn(async move {
228 let _ = stdin.write_all(content.as_bytes()).await;
229 let _ = stdin.shutdown().await;
230 });
231 }
232 }
233
234 let mut stdout_content = String::new();
236 if let Some(mut stdout) = child.stdout.take() {
237 stdout.read_to_string(&mut stdout_content).await?;
238 }
239
240 let mut stderr_content = String::new();
242 if let Some(mut stderr) = child.stderr.take() {
243 stderr.read_to_string(&mut stderr_content).await?;
244 }
245
246 if is_last && self.mirror {
248 if !stdout_content.is_empty() {
249 print!("{}", stdout_content);
250 }
251 if !stderr_content.is_empty() {
252 eprint!("{}", stderr_content);
253 }
254 }
255
256 let status = child.wait().await?;
258 let code = status.code().unwrap_or(-1);
259
260 accumulated_stderr.push_str(&stderr_content);
261 exit_codes.push(code);
262
263 current_stdin = Some(stdout_content.clone());
265 last_result = CommandResult::new(stdout_content, "", code);
266 }
267
268 Ok(CommandResult::new(
269 last_result.stdout,
270 accumulated_stderr,
271 pipeline_exit_code(&exit_codes, pipefail),
272 ))
273 }
274
275 async fn try_virtual_command(
277 &self,
278 cmd_name: &str,
279 full_cmd: &str,
280 stdin: &Option<String>,
281 cwd: Option<&PathBuf>,
282 env: Option<&HashMap<String, String>>,
283 ) -> Option<VirtualCommandResult> {
284 let words = crate::shell_parser::split_command_words(full_cmd);
287 let args: Vec<String> = words.into_iter().skip(1).collect();
288
289 let ctx = crate::commands::CommandContext {
290 args,
291 stdin: stdin.clone(),
292 cwd: cwd.cloned(),
293 env: env.cloned(),
294 output_tx: None,
295 is_cancelled: None,
296 };
297
298 let (result, cd_context) = match cmd_name {
299 "echo" => (crate::commands::echo(ctx).await, None),
300 "pwd" => (crate::commands::pwd(ctx).await, None),
301 "cd" => crate::commands::cd::resolve_cd(ctx).await,
302 "true" => (crate::commands::r#true(ctx).await, None),
303 "false" => (crate::commands::r#false(ctx).await, None),
304 "sleep" => (crate::commands::sleep(ctx).await, None),
305 "cat" => (crate::commands::cat(ctx).await, None),
306 "ls" => (crate::commands::ls(ctx).await, None),
307 "mkdir" => (crate::commands::mkdir(ctx).await, None),
308 "rm" => (crate::commands::rm(ctx).await, None),
309 "touch" => (crate::commands::touch(ctx).await, None),
310 "cp" => (crate::commands::cp(ctx).await, None),
311 "mv" => (crate::commands::mv(ctx).await, None),
312 "basename" => (crate::commands::basename(ctx).await, None),
313 "dirname" => (crate::commands::dirname(ctx).await, None),
314 "env" => (crate::commands::env(ctx).await, None),
315 "exit" => (crate::commands::exit(ctx).await, None),
316 "which" => (crate::commands::which(ctx).await, None),
317 "yes" => (crate::commands::yes(ctx).await, None),
318 "seq" => (crate::commands::seq(ctx).await, None),
319 "tee" => (crate::commands::tee(ctx).await, None),
320 "test" => (crate::commands::test(ctx).await, None),
321 _ => return None,
322 };
323 Some(VirtualCommandResult { result, cd_context })
324 }
325}
326
327pub trait PipelineExt {
329 fn pipe(self, command: impl Into<String>) -> PipelineBuilder;
331}
332
333impl PipelineExt for crate::ProcessRunner {
334 fn pipe(self, command: impl Into<String>) -> PipelineBuilder {
335 PipelineBuilder {
336 first: self,
337 additional: vec![command.into()],
338 }
339 }
340}
341
342pub struct PipelineBuilder {
344 first: crate::ProcessRunner,
345 additional: Vec<String>,
346}
347
348impl PipelineBuilder {
349 pub fn pipe(mut self, command: impl Into<String>) -> Self {
351 self.additional.push(command.into());
352 self
353 }
354
355 pub async fn run(mut self) -> Result<CommandResult> {
357 let first_result = self.first.run().await?;
359 let pipefail = crate::get_shell_settings().await.pipefail;
360 let mut exit_codes = vec![first_result.code];
361
362 let mut current_stdin = Some(first_result.stdout.to_string());
364 let mut accumulated_stderr = first_result.stderr.to_string();
365 let mut last_result = first_result;
366
367 for cmd_str in &self.additional {
368 let mut runner = crate::ProcessRunner::new(
369 cmd_str.clone(),
370 RunOptions {
371 stdin: StdinOption::Content(current_stdin.take().unwrap_or_default()),
372 mirror: false,
373 capture: true,
374 ..Default::default()
375 },
376 );
377
378 let result = runner.run().await?;
379 accumulated_stderr.push_str(&result.stderr);
380 exit_codes.push(result.code);
381
382 current_stdin = Some(result.stdout.to_string());
383 last_result = result;
384 }
385
386 Ok(CommandResult::new(
387 last_result.stdout,
388 accumulated_stderr,
389 pipeline_exit_code(&exit_codes, pipefail),
390 ))
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::pipeline_exit_code;
397
398 #[test]
399 fn pipeline_status_uses_last_stage_by_default() {
400 assert_eq!(pipeline_exit_code(&[3, 0], false), 0);
401 }
402
403 #[test]
404 fn pipefail_uses_rightmost_failing_stage() {
405 assert_eq!(pipeline_exit_code(&[2, 7, 0], true), 7);
406 }
407}