Skip to main content

shrub_core/
results.rs

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