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    borrow::Cow,
14    process::ExitStatus,
15    str,
16    time::Duration,
17};
18
19/// Captured output and status information from a finished command.
20///
21/// `CommandOutput` stores retained raw stdout and stderr bytes. When the runner
22/// is configured with per-stream capture limits, the retained bytes may be a
23/// prefix of the full output; use [`Self::stdout_truncated`] and
24/// [`Self::stderr_truncated`] to detect that case. [`Self::stdout`] and
25/// [`Self::stderr`] return raw bytes exactly as retained. Use
26/// [`Self::stdout_text`] and [`Self::stderr_text`] for strict UTF-8 text, or
27/// [`Self::stdout_lossy_text`] and [`Self::stderr_lossy_text`] to replace
28/// invalid byte sequences 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}
45
46impl CommandOutput {
47    /// Creates command output from captured process data.
48    ///
49    /// # Parameters
50    ///
51    /// * `status` - Process exit status.
52    /// * `stdout` - Captured standard output bytes.
53    /// * `stderr` - Captured standard error bytes.
54    /// * `stdout_truncated` - Whether stdout exceeded the capture limit.
55    /// * `stderr_truncated` - Whether stderr exceeded the capture limit.
56    /// * `elapsed` - Observed process duration.
57    /// # Returns
58    ///
59    /// A command output value containing the supplied data.
60    #[inline]
61    pub(crate) fn new(
62        status: ExitStatus,
63        stdout: Vec<u8>,
64        stderr: Vec<u8>,
65        stdout_truncated: bool,
66        stderr_truncated: bool,
67        elapsed: Duration,
68    ) -> Self {
69        Self {
70            status,
71            stdout,
72            stderr,
73            stdout_truncated,
74            stderr_truncated,
75            elapsed,
76        }
77    }
78
79    /// Returns the command exit code.
80    ///
81    /// # Returns
82    ///
83    /// `Some(code)` when the platform reports a numeric process exit code, or
84    /// `None` when the process ended in a way that does not map to a numeric
85    /// code.
86    #[inline]
87    pub fn exit_code(&self) -> Option<i32> {
88        self.status.code()
89    }
90
91    /// Returns the full process exit status.
92    ///
93    /// # Returns
94    ///
95    /// Platform-specific process exit status reported by the operating system.
96    #[inline]
97    pub const fn exit_status(&self) -> &ExitStatus {
98        &self.status
99    }
100
101    /// Returns the signal that terminated the process on Unix platforms.
102    ///
103    /// # Returns
104    ///
105    /// `Some(signal)` when the process was terminated by a signal, otherwise
106    /// `None`.
107    #[cfg(unix)]
108    #[inline]
109    pub fn termination_signal(&self) -> Option<i32> {
110        self.status.signal()
111    }
112
113    /// Returns captured standard output bytes.
114    ///
115    /// # Returns
116    ///
117    /// A borrowed slice containing stdout exactly as emitted by the process.
118    #[inline]
119    pub fn stdout(&self) -> &[u8] {
120        &self.stdout
121    }
122
123    /// Returns captured standard error bytes.
124    ///
125    /// # Returns
126    ///
127    /// A borrowed slice containing stderr exactly as emitted by the process.
128    #[inline]
129    pub fn stderr(&self) -> &[u8] {
130        &self.stderr
131    }
132
133    /// Returns captured standard output as strict UTF-8 text.
134    ///
135    /// # Returns
136    ///
137    /// `Ok(&str)` when stdout is valid UTF-8.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`str::Utf8Error`] when stdout contains invalid UTF-8.
142    #[inline]
143    pub fn stdout_text(&self) -> Result<&str, str::Utf8Error> {
144        str::from_utf8(&self.stdout)
145    }
146
147    /// Returns captured standard error as strict UTF-8 text.
148    ///
149    /// # Returns
150    ///
151    /// `Ok(&str)` when stderr is valid UTF-8.
152    ///
153    /// # Errors
154    ///
155    /// Returns [`str::Utf8Error`] when stderr contains invalid UTF-8.
156    #[inline]
157    pub fn stderr_text(&self) -> Result<&str, str::Utf8Error> {
158        str::from_utf8(&self.stderr)
159    }
160
161    /// Returns captured standard output as UTF-8 text, replacing invalid bytes.
162    ///
163    /// # Returns
164    ///
165    /// Borrowed UTF-8 text when stdout is valid UTF-8, or an owned string with
166    /// invalid byte sequences replaced by the Unicode replacement character.
167    #[inline]
168    pub fn stdout_lossy_text(&self) -> Cow<'_, str> {
169        String::from_utf8_lossy(&self.stdout)
170    }
171
172    /// Returns captured standard error as UTF-8 text, replacing invalid bytes.
173    ///
174    /// # Returns
175    ///
176    /// Borrowed UTF-8 text when stderr is valid UTF-8, or an owned string with
177    /// invalid byte sequences replaced by the Unicode replacement character.
178    #[inline]
179    pub fn stderr_lossy_text(&self) -> Cow<'_, str> {
180        String::from_utf8_lossy(&self.stderr)
181    }
182
183    /// Returns the observed command duration.
184    ///
185    /// # Returns
186    ///
187    /// Duration from process spawn to observed termination.
188    #[inline]
189    pub const fn elapsed(&self) -> Duration {
190        self.elapsed
191    }
192
193    /// Returns whether captured stdout was truncated by a configured limit.
194    ///
195    /// # Returns
196    ///
197    /// `true` when stdout emitted more bytes than the runner retained.
198    #[inline]
199    pub const fn stdout_truncated(&self) -> bool {
200        self.stdout_truncated
201    }
202
203    /// Returns whether captured stderr was truncated by a configured limit.
204    ///
205    /// # Returns
206    ///
207    /// `true` when stderr emitted more bytes than the runner retained.
208    #[inline]
209    pub const fn stderr_truncated(&self) -> bool {
210        self.stderr_truncated
211    }
212}