Skip to main content

qubit_command/
command_output.rs

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