Skip to main content

qubit_command/
command_output.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9#[cfg(unix)]
10use std::os::unix::process::ExitStatusExt;
11use std::{
12    process::ExitStatus,
13    str,
14    time::Duration,
15};
16
17/// Captured output and status information from a finished command.
18///
19/// `CommandOutput` stores retained raw stdout and stderr bytes. When the runner
20/// is configured with per-stream capture limits, the retained bytes may be a
21/// prefix of the full output; use [`Self::stdout_truncated`] and
22/// [`Self::stderr_truncated`] to detect that case. By default, [`Self::stdout`]
23/// and [`Self::stderr`] validate retained bytes as UTF-8 and return
24/// [`str::Utf8Error`] for invalid output. If the command was run with
25/// [`CommandRunner::lossy_output`](crate::CommandRunner::lossy_output) enabled,
26/// the runner also stores lossy UTF-8 text where invalid byte sequences are
27/// replaced with the Unicode replacement character.
28///
29/// # Author
30///
31/// Haixing Hu
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct CommandOutput {
34    /// Exit status reported by the process.
35    status: ExitStatus,
36    /// Captured standard output bytes.
37    stdout: Vec<u8>,
38    /// Captured standard error bytes.
39    stderr: Vec<u8>,
40    /// Whether stdout was truncated by the configured capture limit.
41    stdout_truncated: bool,
42    /// Whether stderr was truncated by the configured capture limit.
43    stderr_truncated: bool,
44    /// Duration from process spawn to observed termination.
45    elapsed: Duration,
46    /// Lossy UTF-8 stdout generated by the runner when configured.
47    stdout_text: Option<String>,
48    /// Lossy UTF-8 stderr generated by the runner when configured.
49    stderr_text: Option<String>,
50}
51
52impl CommandOutput {
53    /// Creates command output from captured process data.
54    ///
55    /// # Parameters
56    ///
57    /// * `status` - Process exit status.
58    /// * `stdout` - Captured standard output bytes.
59    /// * `stderr` - Captured standard error bytes.
60    /// * `stdout_truncated` - Whether stdout exceeded the capture limit.
61    /// * `stderr_truncated` - Whether stderr exceeded the capture limit.
62    /// * `elapsed` - Observed process duration.
63    /// * `lossy_output` - Whether text accessors should use lossy UTF-8.
64    ///
65    /// # Returns
66    ///
67    /// A command output value containing the supplied data.
68    #[inline]
69    pub(crate) fn new(
70        status: ExitStatus,
71        stdout: Vec<u8>,
72        stderr: Vec<u8>,
73        stdout_truncated: bool,
74        stderr_truncated: bool,
75        elapsed: Duration,
76        lossy_output: bool,
77    ) -> Self {
78        let stdout_text = if lossy_output {
79            Some(String::from_utf8_lossy(&stdout).into_owned())
80        } else {
81            None
82        };
83        let stderr_text = if lossy_output {
84            Some(String::from_utf8_lossy(&stderr).into_owned())
85        } else {
86            None
87        };
88        Self {
89            status,
90            stdout,
91            stderr,
92            stdout_truncated,
93            stderr_truncated,
94            elapsed,
95            stdout_text,
96            stderr_text,
97        }
98    }
99
100    /// Returns the command exit code.
101    ///
102    /// # Returns
103    ///
104    /// `Some(code)` when the platform reports a numeric process exit code, or
105    /// `None` when the process ended in a way that does not map to a numeric
106    /// code.
107    #[inline]
108    pub fn exit_code(&self) -> Option<i32> {
109        self.status.code()
110    }
111
112    /// Returns the full process exit status.
113    ///
114    /// # Returns
115    ///
116    /// Platform-specific process exit status reported by the operating system.
117    #[inline]
118    pub const fn exit_status(&self) -> &ExitStatus {
119        &self.status
120    }
121
122    /// Returns the signal that terminated the process on Unix platforms.
123    ///
124    /// # Returns
125    ///
126    /// `Some(signal)` when the process was terminated by a signal, otherwise
127    /// `None`.
128    #[cfg(unix)]
129    #[inline]
130    pub fn termination_signal(&self) -> Option<i32> {
131        self.status.signal()
132    }
133
134    /// Returns captured standard output as UTF-8 text.
135    ///
136    /// # Returns
137    ///
138    /// `Ok(&str)` when stdout is valid UTF-8. If the command runner used lossy
139    /// output mode, this returns the stored lossy text even when the original
140    /// bytes were not valid UTF-8.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`str::Utf8Error`] when stdout contains invalid UTF-8 and the
145    /// command runner did not enable lossy output mode.
146    #[inline]
147    pub fn stdout(&self) -> Result<&str, str::Utf8Error> {
148        match &self.stdout_text {
149            Some(text) => Ok(text.as_str()),
150            None => str::from_utf8(&self.stdout),
151        }
152    }
153
154    /// Returns captured standard error as UTF-8 text.
155    ///
156    /// # Returns
157    ///
158    /// `Ok(&str)` when stderr is valid UTF-8. If the command runner used lossy
159    /// output mode, this returns the stored lossy text even when the original
160    /// bytes were not valid UTF-8.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`str::Utf8Error`] when stderr contains invalid UTF-8 and the
165    /// command runner did not enable lossy output mode.
166    #[inline]
167    pub fn stderr(&self) -> Result<&str, str::Utf8Error> {
168        match &self.stderr_text {
169            Some(text) => Ok(text.as_str()),
170            None => str::from_utf8(&self.stderr),
171        }
172    }
173
174    /// Returns the observed command duration.
175    ///
176    /// # Returns
177    ///
178    /// Duration from process spawn to observed termination.
179    #[inline]
180    pub const fn elapsed(&self) -> Duration {
181        self.elapsed
182    }
183
184    /// Returns the captured standard output bytes.
185    ///
186    /// # Returns
187    ///
188    /// A borrowed slice containing stdout exactly as emitted by the process.
189    #[inline]
190    pub fn stdout_bytes(&self) -> &[u8] {
191        &self.stdout
192    }
193
194    /// Returns the captured standard error bytes.
195    ///
196    /// # Returns
197    ///
198    /// A borrowed slice containing stderr exactly as emitted by the process.
199    #[inline]
200    pub fn stderr_bytes(&self) -> &[u8] {
201        &self.stderr
202    }
203
204    /// Returns whether captured stdout was truncated by a configured limit.
205    ///
206    /// # Returns
207    ///
208    /// `true` when stdout emitted more bytes than the runner retained.
209    #[inline]
210    pub const fn stdout_truncated(&self) -> bool {
211        self.stdout_truncated
212    }
213
214    /// Returns whether captured stderr was truncated by a configured limit.
215    ///
216    /// # Returns
217    ///
218    /// `true` when stderr emitted more bytes than the runner retained.
219    #[inline]
220    pub const fn stderr_truncated(&self) -> bool {
221        self.stderr_truncated
222    }
223}