Skip to main content

bare_script/sync/
pipeline.rs

1//! Pipeline support for chaining commands together.
2//!
3//! This module provides a type-safe way to create command pipelines
4//! where the output of one command is piped to the input of the next.
5
6use std::process::{Command, Stdio};
7
8use crate::error::{ScriptError, ScriptResult};
9use crate::output::Output;
10
11/// A builder for creating command pipelines.
12///
13/// # Example
14///
15/// ```rust
16/// use bare_script::sync::Pipeline;
17///
18/// #[cfg(not(windows))]
19/// {
20/// let output = Pipeline::new("echo")
21///     .arg("hello world")
22///     .pipe("grep")
23///     .arg("hello")
24///     .execute();
25///     
26/// assert!(output.is_ok());
27/// }
28/// ```
29#[derive(Debug)]
30pub struct Pipeline {
31    commands: Vec<Command>,
32}
33
34impl Pipeline {
35    /// Creates a new pipeline starting with the specified program.
36    pub fn new<S: AsRef<std::ffi::OsStr>>(program: S) -> Self {
37        let mut cmd = Command::new(program);
38        let _ = cmd.stdin(Stdio::piped());
39        Self {
40            commands: vec![cmd],
41        }
42    }
43
44    /// Adds an argument to the last command in the pipeline.
45    pub fn arg<S: AsRef<std::ffi::OsStr>>(mut self, arg: S) -> Self {
46        if let Some(cmd) = self.commands.last_mut() {
47            let _ = cmd.arg(arg);
48        }
49        self
50    }
51
52    /// Adds multiple arguments to the last command in the pipeline.
53    pub fn args<I, S>(mut self, args: I) -> Self
54    where
55        I: IntoIterator<Item = S>,
56        S: AsRef<std::ffi::OsStr>,
57    {
58        if let Some(cmd) = self.commands.last_mut() {
59            let _ = cmd.args(args);
60        }
61        self
62    }
63
64    /// Adds a new command to the pipeline, piping the output of the previous
65    /// command to the stdin of this command.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the previous command's stdout could not be configured
70    /// for piping.
71    pub fn pipe<S: AsRef<std::ffi::OsStr>>(mut self, program: S) -> Self {
72        // Configure the previous command's stdout to be piped
73        if let Some(prev_cmd) = self.commands.last_mut() {
74            let _ = prev_cmd.stdout(Stdio::piped());
75        }
76
77        // Create the new command with piped stdin
78        let mut cmd = Command::new(program);
79        let _ = cmd.stdin(Stdio::piped());
80        self.commands.push(cmd);
81
82        self
83    }
84
85    /// Sets an environment variable for all commands in the pipeline.
86    pub fn env<K, V>(mut self, key: K, value: V) -> Self
87    where
88        K: AsRef<std::ffi::OsStr>,
89        V: AsRef<std::ffi::OsStr>,
90    {
91        for cmd in &mut self.commands {
92            let _ = cmd.env(key.as_ref(), value.as_ref());
93        }
94        self
95    }
96
97    /// Sets the working directory for all commands in the pipeline.
98    pub fn current_dir<D>(mut self, dir: D) -> Self
99    where
100        D: AsRef<std::path::Path>,
101    {
102        for cmd in &mut self.commands {
103            let _ = cmd.current_dir(dir.as_ref());
104        }
105        self
106    }
107
108    /// Configures the pipeline to capture the final output.
109    ///
110    /// This must be called before `execute()` to capture stdout/stderr.
111    pub fn capture_output(mut self) -> Self {
112        if let Some(cmd) = self.commands.last_mut() {
113            let _ = cmd.stdout(Stdio::piped());
114            let _ = cmd.stderr(Stdio::piped());
115        }
116        self
117    }
118
119    /// Executes the pipeline and returns the output of the final command.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if any command in the pipeline fails to execute.
124    pub fn execute(&mut self) -> ScriptResult<Output> {
125        if self.commands.is_empty() {
126            return Err(ScriptError::PipelineEmpty);
127        }
128
129        let num_commands = self.commands.len();
130        let mut previous_child: Option<std::process::Child> = None;
131
132        for i in 0..num_commands {
133            let cmd = &mut self.commands[i];
134
135            if i > 0 {
136                // Connect previous command's stdout to this command's stdin
137                if let Some(ref mut prev) = previous_child {
138                    if let Some(stdout) = prev.stdout.take() {
139                        let _ = cmd.stdin(stdout);
140                    }
141                }
142            }
143
144            if i == num_commands - 1 {
145                // Last command - execute and capture output
146                let output = cmd.output().map_err(ScriptError::IoError)?;
147                return Ok(Output::new(output.stdout, output.stderr, output.status));
148            } else {
149                // Not the last command - spawn and continue
150                let child = cmd.spawn().map_err(ScriptError::IoError)?;
151                previous_child = Some(child);
152            }
153        }
154
155        Err(ScriptError::PipelineError(
156            "Pipeline execution failed".into(),
157        ))
158    }
159}
160
161impl Default for Pipeline {
162    fn default() -> Self {
163        Self::new("")
164    }
165}