qubit-command 0.1.0

Command-line process running utilities for Rust
Documentation
/*******************************************************************************
 *
 *    Copyright (c) 2026.
 *    Haixing Hu, Qubit Co. Ltd.
 *
 *    All rights reserved.
 *
 ******************************************************************************/
use std::{
    str,
    time::Duration,
};

/// Captured output and status information from a finished command.
///
/// `CommandOutput` always stores raw stdout and stderr bytes. By default,
/// [`Self::stdout`] and [`Self::stderr`] validate those bytes as UTF-8 and
/// return [`str::Utf8Error`] for invalid output. If the command was run with
/// [`CommandRunner::lossy_output`](crate::CommandRunner::lossy_output) enabled,
/// the runner also stores lossy UTF-8 text where invalid byte sequences are
/// replaced with the Unicode replacement character. This makes the text
/// accessors return `Ok(&str)` while preserving the original bytes through
/// [`Self::stdout_bytes`] and [`Self::stderr_bytes`].
///
/// # Author
///
/// Haixing Hu
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandOutput {
    /// Exit code reported by the process, or `None` when the platform could not
    /// represent termination as a numeric code.
    exit_code: Option<i32>,
    /// Captured standard output bytes.
    stdout: Vec<u8>,
    /// Captured standard error bytes.
    stderr: Vec<u8>,
    /// Duration from process spawn to observed termination.
    elapsed: Duration,
    /// Lossy UTF-8 stdout generated by the runner when configured.
    stdout_text: Option<String>,
    /// Lossy UTF-8 stderr generated by the runner when configured.
    stderr_text: Option<String>,
}

impl CommandOutput {
    /// Creates command output from captured process data.
    ///
    /// # Parameters
    ///
    /// * `exit_code` - Numeric process exit code, if available.
    /// * `stdout` - Captured standard output bytes.
    /// * `stderr` - Captured standard error bytes.
    /// * `elapsed` - Observed process duration.
    /// * `lossy_output` - Whether text accessors should use lossy UTF-8.
    ///
    /// # Returns
    ///
    /// A command output value containing the supplied data.
    #[inline]
    pub(crate) fn new(
        exit_code: Option<i32>,
        stdout: Vec<u8>,
        stderr: Vec<u8>,
        elapsed: Duration,
        lossy_output: bool,
    ) -> Self {
        let stdout_text = if lossy_output {
            Some(String::from_utf8_lossy(&stdout).into_owned())
        } else {
            None
        };
        let stderr_text = if lossy_output {
            Some(String::from_utf8_lossy(&stderr).into_owned())
        } else {
            None
        };
        Self {
            exit_code,
            stdout,
            stderr,
            elapsed,
            stdout_text,
            stderr_text,
        }
    }

    /// Returns the command exit code.
    ///
    /// # Returns
    ///
    /// `Some(code)` when the platform reports a numeric process exit code, or
    /// `None` when the process ended in a way that does not map to a numeric
    /// code.
    #[inline]
    pub const fn exit_code(&self) -> Option<i32> {
        self.exit_code
    }

    /// Returns captured standard output as UTF-8 text.
    ///
    /// # Returns
    ///
    /// `Ok(&str)` when stdout is valid UTF-8. If the command runner used lossy
    /// output mode, this returns the stored lossy text even when the original
    /// bytes were not valid UTF-8.
    ///
    /// # Errors
    ///
    /// Returns [`str::Utf8Error`] when stdout contains invalid UTF-8 and the
    /// command runner did not enable lossy output mode.
    #[inline]
    pub fn stdout(&self) -> Result<&str, str::Utf8Error> {
        match &self.stdout_text {
            Some(text) => Ok(text.as_str()),
            None => str::from_utf8(&self.stdout),
        }
    }

    /// Returns captured standard error as UTF-8 text.
    ///
    /// # Returns
    ///
    /// `Ok(&str)` when stderr is valid UTF-8. If the command runner used lossy
    /// output mode, this returns the stored lossy text even when the original
    /// bytes were not valid UTF-8.
    ///
    /// # Errors
    ///
    /// Returns [`str::Utf8Error`] when stderr contains invalid UTF-8 and the
    /// command runner did not enable lossy output mode.
    #[inline]
    pub fn stderr(&self) -> Result<&str, str::Utf8Error> {
        match &self.stderr_text {
            Some(text) => Ok(text.as_str()),
            None => str::from_utf8(&self.stderr),
        }
    }

    /// Returns the observed command duration.
    ///
    /// # Returns
    ///
    /// Duration from process spawn to observed termination.
    #[inline]
    pub const fn elapsed(&self) -> Duration {
        self.elapsed
    }

    /// Returns the captured standard output bytes.
    ///
    /// # Returns
    ///
    /// A borrowed slice containing stdout exactly as emitted by the process.
    #[inline]
    pub fn stdout_bytes(&self) -> &[u8] {
        &self.stdout
    }

    /// Returns the captured standard error bytes.
    ///
    /// # Returns
    ///
    /// A borrowed slice containing stderr exactly as emitted by the process.
    #[inline]
    pub fn stderr_bytes(&self) -> &[u8] {
        &self.stderr
    }
}