bare-script 0.1.1

The type-safe scripting authority for Rust. A framework for building robust shell commands and automation with 'Parse, don't validate' philosophy.
Documentation
//! Pipeline support for chaining commands together.
//!
//! This module provides a type-safe way to create command pipelines
//! where the output of one command is piped to the input of the next.

use std::process::{Command, Stdio};

use crate::error::{ScriptError, ScriptResult};
use crate::output::Output;

/// A builder for creating command pipelines.
///
/// # Example
///
/// ```rust
/// use bare_script::sync::Pipeline;
///
/// #[cfg(not(windows))]
/// {
/// let output = Pipeline::new("echo")
///     .arg("hello world")
///     .pipe("grep")
///     .arg("hello")
///     .execute();
///     
/// assert!(output.is_ok());
/// }
/// ```
#[derive(Debug)]
pub struct Pipeline {
    commands: Vec<Command>,
}

impl Pipeline {
    /// Creates a new pipeline starting with the specified program.
    pub fn new<S: AsRef<std::ffi::OsStr>>(program: S) -> Self {
        let mut cmd = Command::new(program);
        let _ = cmd.stdin(Stdio::piped());
        Self {
            commands: vec![cmd],
        }
    }

    /// Adds an argument to the last command in the pipeline.
    pub fn arg<S: AsRef<std::ffi::OsStr>>(mut self, arg: S) -> Self {
        if let Some(cmd) = self.commands.last_mut() {
            let _ = cmd.arg(arg);
        }
        self
    }

    /// Adds multiple arguments to the last command in the pipeline.
    pub fn args<I, S>(mut self, args: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
    {
        if let Some(cmd) = self.commands.last_mut() {
            let _ = cmd.args(args);
        }
        self
    }

    /// Adds a new command to the pipeline, piping the output of the previous
    /// command to the stdin of this command.
    ///
    /// # Errors
    ///
    /// Returns an error if the previous command's stdout could not be configured
    /// for piping.
    pub fn pipe<S: AsRef<std::ffi::OsStr>>(mut self, program: S) -> Self {
        // Configure the previous command's stdout to be piped
        if let Some(prev_cmd) = self.commands.last_mut() {
            let _ = prev_cmd.stdout(Stdio::piped());
        }

        // Create the new command with piped stdin
        let mut cmd = Command::new(program);
        let _ = cmd.stdin(Stdio::piped());
        self.commands.push(cmd);

        self
    }

    /// Sets an environment variable for all commands in the pipeline.
    pub fn env<K, V>(mut self, key: K, value: V) -> Self
    where
        K: AsRef<std::ffi::OsStr>,
        V: AsRef<std::ffi::OsStr>,
    {
        for cmd in &mut self.commands {
            let _ = cmd.env(key.as_ref(), value.as_ref());
        }
        self
    }

    /// Sets the working directory for all commands in the pipeline.
    pub fn current_dir<D>(mut self, dir: D) -> Self
    where
        D: AsRef<std::path::Path>,
    {
        for cmd in &mut self.commands {
            let _ = cmd.current_dir(dir.as_ref());
        }
        self
    }

    /// Configures the pipeline to capture the final output.
    ///
    /// This must be called before `execute()` to capture stdout/stderr.
    pub fn capture_output(mut self) -> Self {
        if let Some(cmd) = self.commands.last_mut() {
            let _ = cmd.stdout(Stdio::piped());
            let _ = cmd.stderr(Stdio::piped());
        }
        self
    }

    /// Executes the pipeline and returns the output of the final command.
    ///
    /// # Errors
    ///
    /// Returns an error if any command in the pipeline fails to execute.
    pub fn execute(&mut self) -> ScriptResult<Output> {
        if self.commands.is_empty() {
            return Err(ScriptError::PipelineEmpty);
        }

        let num_commands = self.commands.len();
        let mut previous_child: Option<std::process::Child> = None;

        for i in 0..num_commands {
            let cmd = &mut self.commands[i];

            if i > 0 {
                // Connect previous command's stdout to this command's stdin
                if let Some(ref mut prev) = previous_child {
                    if let Some(stdout) = prev.stdout.take() {
                        let _ = cmd.stdin(stdout);
                    }
                }
            }

            if i == num_commands - 1 {
                // Last command - execute and capture output
                let output = cmd.output().map_err(ScriptError::IoError)?;
                return Ok(Output::new(output.stdout, output.stderr, output.status));
            } else {
                // Not the last command - spawn and continue
                let child = cmd.spawn().map_err(ScriptError::IoError)?;
                previous_child = Some(child);
            }
        }

        Err(ScriptError::PipelineError(
            "Pipeline execution failed".into(),
        ))
    }
}

impl Default for Pipeline {
    fn default() -> Self {
        Self::new("")
    }
}