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
//! Synchronous command execution module.
//!
//! This module provides a type-safe interface for executing shell commands
//! using the standard library's `std::process::Command`.

use std::ffi::OsStr;
use std::io;
use std::process::{Command, ExitStatus, Stdio};

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

/// Command builder for synchronous execution.
#[derive(Debug)]
pub struct CommandBuilder {
    cmd: Command,
}

impl CommandBuilder {
    /// Creates a new command builder for the specified program.
    pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
        Self {
            cmd: Command::new(program),
        }
    }

    /// Adds a single argument to the command.
    pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
        let _ = self.cmd.arg(arg);
        self
    }

    /// Adds multiple arguments to the command.
    pub fn args<I, S>(mut self, args: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let _ = self.cmd.args(args);
        self
    }

    /// Sets an environment variable for the command.
    pub fn env<K, V>(mut self, key: K, value: V) -> Self
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        let _ = self.cmd.env(key, value);
        self
    }

    /// Sets multiple environment variables for the command.
    pub fn envs<I, K, V>(mut self, vars: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        let _ = self.cmd.envs(vars);
        self
    }

    /// Clears all environment variables inherited from the parent process.
    pub fn env_clear(mut self) -> Self {
        let _ = self.cmd.env_clear();
        self
    }

    /// Removes an environment variable from the command's environment.
    pub fn env_remove<K>(mut self, key: K) -> Self
    where
        K: AsRef<OsStr>,
    {
        let _ = self.cmd.env_remove(key);
        self
    }

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

    /// Configures the stdin handle for the command.
    pub fn stdin<T: Into<Stdio>>(mut self, stdin: T) -> Self {
        let _ = self.cmd.stdin(stdin);
        self
    }

    /// Configures the stdout handle for the command.
    pub fn stdout<T: Into<Stdio>>(mut self, stdout: T) -> Self {
        let _ = self.cmd.stdout(stdout);
        self
    }

    /// Configures the stderr handle for the command.
    pub fn stderr<T: Into<Stdio>>(mut self, stderr: T) -> Self {
        let _ = self.cmd.stderr(stderr);
        self
    }

    /// Configures the command to capture both stdout and stderr.
    pub fn capture_output(mut self) -> Self {
        let _ = self.cmd.stdout(Stdio::piped());
        let _ = self.cmd.stderr(Stdio::piped());
        self
    }

    /// Configures the command to inherit stdin from the parent process.
    pub fn inherit_stdin(mut self) -> Self {
        let _ = self.cmd.stdin(Stdio::inherit());
        self
    }

    /// Configures the command to inherit stdout from the parent process.
    pub fn inherit_stdout(mut self) -> Self {
        let _ = self.cmd.stdout(Stdio::inherit());
        self
    }

    /// Configures the command to inherit stderr from the parent process.
    pub fn inherit_stderr(mut self) -> Self {
        let _ = self.cmd.stderr(Stdio::inherit());
        self
    }

    /// Configures the command to read from null stdin.
    pub fn null_stdin(mut self) -> Self {
        let _ = self.cmd.stdin(Stdio::null());
        self
    }

    /// Configures the command to write to null stdout.
    pub fn null_stdout(mut self) -> Self {
        let _ = self.cmd.stdout(Stdio::null());
        self
    }

    /// Configures the command to write to null stderr.
    pub fn null_stderr(mut self) -> Self {
        let _ = self.cmd.stderr(Stdio::null());
        self
    }

    /// Sets the process group ID of the child process.
    ///
    /// A `pgroup` of 0 will cause the child to be placed in a new process group.
    ///
    /// # Errors
    ///
    /// Returns an error if the process group could not be set.
    #[cfg(unix)]
    pub fn process_group(mut self, pgroup: i32) -> Self {
        use std::os::unix::process::CommandExt;
        self.cmd.process_group(pgroup);
        self
    }

    /// Sets whether the child process should create a new session.
    ///
    /// If `create` is `true`, the child process will call `setsid` to create
    /// a new session and become the session leader.
    ///
    /// # Errors
    ///
    /// Returns an error if `setsid` fails.
    #[cfg(unix)]
    pub fn setsid(mut self, create: bool) -> Self {
        use std::os::unix::process::CommandExt;
        self.cmd.setsid(create);
        self
    }

    /// Sets the child process's user ID.
    ///
    /// This translates to a `setuid` call in the child process.
    /// Failure in the `setuid` call will cause the spawn to fail.
    ///
    /// # Errors
    ///
    /// Returns an error if the UID could not be set.
    #[cfg(unix)]
    pub fn uid(mut self, id: u32) -> Self {
        use std::os::unix::process::CommandExt;
        self.cmd.uid(id);
        self
    }

    /// Sets the child process's group ID.
    ///
    /// This translates to a `setgid` call in the child process.
    /// Failure in the `setgid` call will cause the spawn to fail.
    ///
    /// # Errors
    ///
    /// Returns an error if the GID could not be set.
    #[cfg(unix)]
    pub fn gid(mut self, id: u32) -> Self {
        use std::os::unix::process::CommandExt;
        self.cmd.gid(id);
        self
    }

    /// Sets the process creation flags (Windows only).
    ///
    /// Sets the process creation flags to be passed to `CreateProcess`.
    /// These will always be ORed with `CREATE_UNICODE_ENVIRONMENT`.
    ///
    /// # Errors
    ///
    /// This method does not return errors directly, but the spawned process
    /// may fail if the flags are invalid.
    #[cfg(windows)]
    pub fn creation_flags(mut self, flags: u32) -> Self {
        use std::os::windows::process::CommandExt;
        let _ = self.cmd.creation_flags(flags);
        self
    }

    /// Sets platform-specific creation flags (no-op on non-Windows).
    ///
    /// Note: On Unix, use `process_group`, `setsid`, `uid`, or `gid` instead.
    #[cfg(not(windows))]
    pub fn creation_flags(self, _flags: u32) -> Self {
        self
    }

    /// Spawns the command as a child process.
    pub fn spawn(&mut self) -> io::Result<std::process::Child> {
        self.cmd.spawn()
    }

    /// Executes the command and captures its output.
    pub fn output(&mut self) -> io::Result<Output> {
        let output = self.cmd.output()?;
        Ok(Output::new(output.stdout, output.stderr, output.status))
    }

    /// Executes the command and captures its output, returning a ScriptResult.
    pub fn execute(&mut self) -> ScriptResult<Output> {
        self.output().map_err(ScriptError::IoError)
    }

    /// Waits for the command to complete and returns its exit status.
    pub fn status(&mut self) -> io::Result<ExitStatus> {
        self.cmd.status()
    }

    /// Executes the command with a timeout.
    ///
    /// If the command does not complete within the specified duration,
    /// the child process is killed and a timeout error is returned.
    ///
    /// Note: The builder is consumed by this operation and cannot be reused.
    ///
    /// # Errors
    ///
    /// Returns [`ScriptError::Timeout`] if the command exceeds the timeout.
    /// Returns [`ScriptError::IoError`] if the command fails to start or execute.
    pub fn execute_with_timeout(self, timeout: std::time::Duration) -> ScriptResult<Output> {
        use std::sync::mpsc;
        use std::thread;

        let (tx, rx) = mpsc::channel();

        // Move the builder into the thread
        let mut builder = self;
        let handle = thread::spawn(move || {
            let result = builder.cmd.output();
            drop(tx.send(result));
        });
        drop(handle);

        match rx.recv_timeout(timeout) {
            Ok(Ok(output)) => Ok(Output::new(output.stdout, output.stderr, output.status)),
            Ok(Err(e)) => Err(ScriptError::IoError(e)),
            Err(mpsc::RecvTimeoutError::Timeout) => Err(ScriptError::Timeout(timeout)),
            Err(mpsc::RecvTimeoutError::Disconnected) => Err(ScriptError::Other(
                "Thread disconnected unexpectedly".into(),
            )),
        }
    }
}