kodegen_bash_shell/core/
results.rs

1//! Encapsulation of execution results.
2
3use tokio_util::sync::CancellationToken;
4
5use super::{error, processes};
6
7/// Represents the result of executing a command or similar item.
8#[derive(Default)]
9pub struct ExecutionResult {
10    /// The control flow transition to apply after execution.
11    pub next_control_flow: ExecutionControlFlow,
12    /// The exit code resulting from execution.
13    pub exit_code: ExecutionExitCode,
14}
15
16impl ExecutionResult {
17    /// Returns a new `ExecutionResult` with the given exit code.
18    ///
19    /// # Arguments
20    ///
21    /// * `exit_code` - The exit code of the command.
22    pub fn new(exit_code: u8) -> Self {
23        Self {
24            exit_code: exit_code.into(),
25            ..Self::default()
26        }
27    }
28
29    /// Returns a new `ExecutionResult` reflecting a process that was stopped.
30    pub fn stopped() -> Self {
31        // TODO: Decide how to sort this out in a platform-independent way.
32        const SIGTSTP: std::os::raw::c_int = 20;
33
34        #[expect(clippy::cast_possible_truncation)]
35        Self::new(128 + SIGTSTP as u8)
36    }
37
38    /// Returns a new `ExecutionResult` with an exit code of 0.
39    pub const fn success() -> Self {
40        Self {
41            next_control_flow: ExecutionControlFlow::Normal,
42            exit_code: ExecutionExitCode::Success,
43        }
44    }
45
46    /// Returns a new `ExecutionResult` with a general error exit code.
47    pub const fn general_error() -> Self {
48        Self {
49            next_control_flow: ExecutionControlFlow::Normal,
50            exit_code: ExecutionExitCode::GeneralError,
51        }
52    }
53
54    /// Returns whether the command was successful.
55    pub const fn is_success(&self) -> bool {
56        self.exit_code.is_success()
57    }
58
59    /// Returns whether the execution result indicates normal control flow.
60    /// Returns `false` if there is any control flow transition requested.
61    pub const fn is_normal_flow(&self) -> bool {
62        matches!(self.next_control_flow, ExecutionControlFlow::Normal)
63    }
64
65    /// Returns whether the execution result indicates a loop break.
66    pub const fn is_break(&self) -> bool {
67        matches!(
68            self.next_control_flow,
69            ExecutionControlFlow::BreakLoop { .. }
70        )
71    }
72
73    /// Returns whether the execution result indicates a loop continue.
74    pub const fn is_continue(&self) -> bool {
75        matches!(
76            self.next_control_flow,
77            ExecutionControlFlow::ContinueLoop { .. }
78        )
79    }
80
81    /// Returns whether the execution result indicates an early return
82    /// from a function or script, or an exit from the shell. Returns `false`
83    /// otherwise, including loop breaks or continues.
84    pub const fn is_return_or_exit(&self) -> bool {
85        matches!(
86            self.next_control_flow,
87            ExecutionControlFlow::ReturnFromFunctionOrScript | ExecutionControlFlow::ExitShell
88        )
89    }
90
91    /// Returns whether the execution was cancelled (exit code 130).
92    pub fn is_cancelled(&self) -> bool {
93        matches!(self.exit_code, ExecutionExitCode::Interrupted)
94    }
95}
96
97impl From<ExecutionExitCode> for ExecutionResult {
98    fn from(exit_code: ExecutionExitCode) -> Self {
99        Self {
100            next_control_flow: ExecutionControlFlow::Normal,
101            exit_code,
102        }
103    }
104}
105
106/// Represents an exit code from execution.
107#[derive(Clone, Copy, Default)]
108pub enum ExecutionExitCode {
109    /// Indicates successful execution.
110    #[default]
111    Success,
112    /// Indicates a general error.
113    GeneralError,
114    /// Indicates invalid usage.
115    InvalidUsage,
116    /// Cannot execute the command.
117    CannotExecute,
118    /// Indicates a command or similar item was not found.
119    NotFound,
120    /// Indicates execution was interrupted.
121    Interrupted,
122    /// Indicates unimplemented functionality was encountered.
123    Unimplemented,
124    /// A custom exit code.
125    Custom(u8),
126}
127
128impl ExecutionExitCode {
129    /// Returns whether the exit code indicates success.
130    pub const fn is_success(&self) -> bool {
131        matches!(self, Self::Success)
132    }
133}
134
135impl From<u8> for ExecutionExitCode {
136    fn from(code: u8) -> Self {
137        match code {
138            0 => Self::Success,
139            1 => Self::GeneralError,
140            2 => Self::InvalidUsage,
141            99 => Self::Unimplemented,
142            126 => Self::CannotExecute,
143            127 => Self::NotFound,
144            130 => Self::Interrupted,
145            code => Self::Custom(code),
146        }
147    }
148}
149
150impl From<ExecutionExitCode> for u8 {
151    fn from(code: ExecutionExitCode) -> Self {
152        Self::from(&code)
153    }
154}
155
156impl From<&ExecutionExitCode> for u8 {
157    fn from(code: &ExecutionExitCode) -> Self {
158        match code {
159            ExecutionExitCode::Success => 0,
160            ExecutionExitCode::GeneralError => 1,
161            ExecutionExitCode::InvalidUsage => 2,
162            ExecutionExitCode::Unimplemented => 99,
163            ExecutionExitCode::CannotExecute => 126,
164            ExecutionExitCode::NotFound => 127,
165            ExecutionExitCode::Interrupted => 130,
166            ExecutionExitCode::Custom(code) => *code,
167        }
168    }
169}
170
171/// Represents a control flow transition to apply.
172#[derive(Clone, Copy, Default)]
173pub enum ExecutionControlFlow {
174    /// Continue normal execution.
175    #[default]
176    Normal,
177    /// Break out of an enclosing loop.
178    BreakLoop {
179        /// Identifies which level of nested loops to break out of. 0 indicates the innermost loop,
180        /// 1 indicates the next outer loop, and so on.
181        levels: usize,
182    },
183    /// Continue to the next iteration of an enclosing loop.
184    ContinueLoop {
185        /// Identifies which level of nested loops to continue. 0 indicates the innermost loop,
186        /// 1 indicates the next outer loop, and so on.
187        levels: usize,
188    },
189    /// Return from the current function or script.
190    ReturnFromFunctionOrScript,
191    /// Exit the shell.
192    ExitShell,
193}
194
195impl ExecutionControlFlow {
196    /// Attempts to decrement the loop levels for `BreakLoop` or `ContinueLoop`.
197    /// If the levels reach zero, transitions to `Normal`. If the control flow is not
198    /// a loop break or continue, no changes are made.
199    #[must_use]
200    pub const fn try_decrement_loop_levels(&self) -> Self {
201        match self {
202            Self::BreakLoop { levels: 0 } | Self::ContinueLoop { levels: 0 } => Self::Normal,
203            Self::BreakLoop { levels } => Self::BreakLoop {
204                levels: *levels - 1,
205            },
206            Self::ContinueLoop { levels } => Self::ContinueLoop {
207                levels: *levels - 1,
208            },
209            control_flow => *control_flow,
210        }
211    }
212}
213
214/// Represents the result of spawning an execution; captures both execution
215/// that immediately returns as well as execution that starts a process
216/// asynchronously.
217pub enum ExecutionSpawnResult {
218    /// Indicates that the execution completed.
219    Completed(ExecutionResult),
220    /// Indicates that a process was started and had not yet completed.
221    StartedProcess(processes::ChildProcess),
222}
223
224impl From<ExecutionResult> for ExecutionSpawnResult {
225    fn from(result: ExecutionResult) -> Self {
226        Self::Completed(result)
227    }
228}
229
230impl ExecutionSpawnResult {
231    /// Waits for the command to complete.
232    ///
233    /// # Arguments
234    ///
235    /// * `no_wait` - If true, do not wait for the command to complete; return immediately.
236    /// * `cancellation_token` - Optional token to cancel the wait operation.
237    pub async fn wait(
238        self,
239        no_wait: bool,
240        cancellation_token: Option<&CancellationToken>,
241    ) -> Result<ExecutionWaitResult, error::Error> {
242        match self {
243            Self::StartedProcess(mut child) => {
244                let process_wait_result = if !no_wait {
245                    // Wait for the process to exit or for a relevant signal, whichever happens
246                    // first.
247                    child.wait(cancellation_token).await?
248                } else {
249                    processes::ProcessWaitResult::Stopped
250                };
251
252                let wait_result = match process_wait_result {
253                    processes::ProcessWaitResult::Completed(output) => {
254                        ExecutionWaitResult::Completed(ExecutionResult::from(output))
255                    }
256                    processes::ProcessWaitResult::Stopped => ExecutionWaitResult::Stopped(child),
257                    processes::ProcessWaitResult::Cancelled => {
258                        ExecutionWaitResult::Cancelled(child)
259                    }
260                };
261
262                Ok(wait_result)
263            }
264            Self::Completed(result) => Ok(ExecutionWaitResult::Completed(result)),
265        }
266    }
267}
268
269/// Represents the result of waiting for an execution to complete.
270pub enum ExecutionWaitResult {
271    /// Indicates that the execution completed.
272    Completed(ExecutionResult),
273    /// Indicates that the execution was stopped.
274    Stopped(processes::ChildProcess),
275    /// Indicates that the execution was cancelled via CancellationToken.
276    Cancelled(processes::ChildProcess),
277}
278
279// ============================================================================
280// Streaming Output Types
281// ============================================================================
282
283/// Identifies the output stream
284#[derive(Clone, Copy, Debug, PartialEq, Eq)]
285pub enum OutputStreamType {
286    Stdout,
287    Stderr,
288}
289
290/// A chunk of output from streaming execution
291#[derive(Clone, Debug)]
292pub struct StreamingOutput {
293    pub stream: OutputStreamType,
294    pub data: Vec<u8>,
295}
296
297impl StreamingOutput {
298    pub fn stdout(data: Vec<u8>) -> Self {
299        Self { stream: OutputStreamType::Stdout, data }
300    }
301
302    pub fn stderr(data: Vec<u8>) -> Self {
303        Self { stream: OutputStreamType::Stderr, data }
304    }
305
306    pub fn is_stdout(&self) -> bool {
307        matches!(self.stream, OutputStreamType::Stdout)
308    }
309
310    pub fn is_stderr(&self) -> bool {
311        matches!(self.stream, OutputStreamType::Stderr)
312    }
313
314    pub fn as_str_lossy(&self) -> std::borrow::Cow<'_, str> {
315        String::from_utf8_lossy(&self.data)
316    }
317}