qubit-command 0.2.1

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

/// Captured output and status information from a finished command.
///
/// `CommandOutput` stores retained raw stdout and stderr bytes. When the runner
/// is configured with per-stream capture limits, the retained bytes may be a
/// prefix of the full output; use [`Self::stdout_truncated`] and
/// [`Self::stderr_truncated`] to detect that case. By default, [`Self::stdout`]
/// and [`Self::stderr`] validate retained 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.
///
/// # Author
///
/// Haixing Hu
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandOutput {
    /// Exit status reported by the process.
    status: ExitStatus,
    /// Captured standard output bytes.
    stdout: Vec<u8>,
    /// Captured standard error bytes.
    stderr: Vec<u8>,
    /// Whether stdout was truncated by the configured capture limit.
    stdout_truncated: bool,
    /// Whether stderr was truncated by the configured capture limit.
    stderr_truncated: bool,
    /// 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
    ///
    /// * `status` - Process exit status.
    /// * `stdout` - Captured standard output bytes.
    /// * `stderr` - Captured standard error bytes.
    /// * `stdout_truncated` - Whether stdout exceeded the capture limit.
    /// * `stderr_truncated` - Whether stderr exceeded the capture limit.
    /// * `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(
        status: ExitStatus,
        stdout: Vec<u8>,
        stderr: Vec<u8>,
        stdout_truncated: bool,
        stderr_truncated: bool,
        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 {
            status,
            stdout,
            stderr,
            stdout_truncated,
            stderr_truncated,
            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 fn exit_code(&self) -> Option<i32> {
        self.status.code()
    }

    /// Returns the full process exit status.
    ///
    /// # Returns
    ///
    /// Platform-specific process exit status reported by the operating system.
    #[inline]
    pub const fn exit_status(&self) -> &ExitStatus {
        &self.status
    }

    /// Returns the signal that terminated the process on Unix platforms.
    ///
    /// # Returns
    ///
    /// `Some(signal)` when the process was terminated by a signal, otherwise
    /// `None`.
    #[cfg(unix)]
    #[inline]
    pub fn termination_signal(&self) -> Option<i32> {
        self.status.signal()
    }

    /// 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
    }

    /// Returns whether captured stdout was truncated by a configured limit.
    ///
    /// # Returns
    ///
    /// `true` when stdout emitted more bytes than the runner retained.
    #[inline]
    pub const fn stdout_truncated(&self) -> bool {
        self.stdout_truncated
    }

    /// Returns whether captured stderr was truncated by a configured limit.
    ///
    /// # Returns
    ///
    /// `true` when stderr emitted more bytes than the runner retained.
    #[inline]
    pub const fn stderr_truncated(&self) -> bool {
        self.stderr_truncated
    }
}