Skip to main content

brush_core/
error.rs

1//! Error facilities
2
3use std::path::PathBuf;
4
5use crate::{Shell, ShellFd, extensions, results, sys};
6
7/// Unified error type for this crate. Contains just a kind for now,
8/// but will be extended later with additional context.
9#[derive(thiserror::Error, Debug)]
10#[error("{kind}")]
11pub struct Error {
12    /// The kind of error.
13    #[source]
14    kind: ErrorKind,
15
16    /// Whether or not the error should be considered a "fatal" error that would
17    /// result in abnormal exit of a non-interactive shell.
18    fatal: bool,
19}
20
21/// Monolithic error type for the shell
22#[derive(thiserror::Error, Debug)]
23pub enum ErrorKind {
24    /// A tilde expression was used without a valid HOME variable
25    #[error("cannot expand tilde expression with HOME not set")]
26    TildeWithoutValidHome,
27
28    /// An attempt was made to assign a list to an array member
29    #[error("cannot assign list to array member")]
30    AssigningListToArrayMember,
31
32    /// An attempt was made to convert an associative array to an indexed array.
33    #[error("cannot convert associative array to indexed array")]
34    ConvertingAssociativeArrayToIndexedArray,
35
36    /// An attempt was made to convert an indexed array to an associative array.
37    #[error("cannot convert indexed array to associative array")]
38    ConvertingIndexedArrayToAssociativeArray,
39
40    /// An error occurred while sourcing the indicated script file.
41    #[error("failed to source file: {0}")]
42    FailedSourcingFile(PathBuf, #[source] std::io::Error),
43
44    /// The shell failed to send a signal to a process.
45    #[error("failed to send signal to process")]
46    FailedToSendSignal,
47
48    /// An attempt was made to assign a value to a special parameter.
49    #[error("cannot assign in this way")]
50    CannotAssignToSpecialParameter,
51
52    /// Checked expansion error.
53    #[error("expansion error: {0}")]
54    CheckedExpansionError(String),
55
56    /// A reference was made to an unknown shell function.
57    #[error("function not found: {0}")]
58    FunctionNotFound(String),
59
60    /// Command was not found.
61    #[error("command not found: {0}")]
62    CommandNotFound(String),
63
64    /// Not a builtin.
65    #[error("not a shell builtin: {0}")]
66    BuiltinNotFound(String),
67
68    /// The working directory does not exist.
69    #[error("working directory does not exist: {0}")]
70    WorkingDirMissing(PathBuf),
71
72    /// Failed to execute command.
73    #[error("failed to execute command '{0}': {1}")]
74    FailedToExecuteCommand(String, #[source] std::io::Error),
75
76    /// History item was not found.
77    #[error("history item not found")]
78    HistoryItemNotFound,
79
80    /// The requested functionality has not yet been implemented in this shell.
81    #[error("not yet implemented: {0}")]
82    Unimplemented(&'static str),
83
84    /// The requested functionality has not yet been implemented in this shell; it is tracked in a
85    /// GitHub issue.
86    #[error("not yet implemented: {0}; see https://github.com/reubeno/brush/issues/{1}")]
87    UnimplementedAndTracked(&'static str, u32),
88
89    /// An expected environment scope could not be found.
90    #[error("missing environment scope")]
91    MissingScope,
92
93    /// The environment scope required for a new variable is not available.
94    #[error("environment scope required for new variable is not available")]
95    MissingScopeForNewVariable,
96
97    /// An unexpected environment scope type was encountered.
98    #[error("unexpected environment scope type: expected '{expected}', found '{actual}'")]
99    UnexpectedScopeType {
100        /// The expected scope type.
101        expected: crate::env::EnvironmentScope,
102        /// The actual scope type.
103        actual: crate::env::EnvironmentScope,
104    },
105
106    /// The given path is not a directory.
107    #[error("not a directory: {0}")]
108    NotADirectory(PathBuf),
109
110    /// The given path is a directory.
111    #[error("path is a directory")]
112    IsADirectory,
113
114    /// The given variable is not an array.
115    #[error("variable is not an array")]
116    NotArray,
117
118    /// The current user could not be determined.
119    #[error("no current user")]
120    NoCurrentUser,
121
122    /// The requested input or output redirection is invalid.
123    #[error("invalid redirection target")]
124    InvalidRedirection,
125
126    /// An error occurred while redirecting input or output with the given file.
127    #[error("failed to redirect to {0}: {1}")]
128    RedirectionFailure(String, String),
129
130    /// An error occurred evaluating an arithmetic expression.
131    #[error("arithmetic evaluation error: {0}")]
132    EvalError(#[from] crate::arithmetic::EvalError),
133
134    /// The given string could not be parsed as an integer.
135    #[error("failed to parse '{s}' as a {int_type_name}, base-{radix} integer: {inner}")]
136    IntParseError {
137        /// The string that failed to parse.
138        s: String,
139        /// The integer type being parsed.
140        int_type_name: &'static str,
141        /// The radix (base) used for parsing.
142        radix: u32,
143        /// The underlying parse error.
144        inner: std::num::ParseIntError,
145    },
146
147    /// The given integer could not be converted to the target type.
148    #[error("integer conversion error")]
149    TryIntParseError(#[from] std::num::TryFromIntError),
150
151    /// A byte sequence could not be decoded as a valid UTF-8 string.
152    #[error("failed to decode utf-8")]
153    FromUtf8Error(#[from] std::string::FromUtf8Error),
154
155    /// A byte sequence could not be decoded as a valid UTF-8 string.
156    #[error("failed to decode utf-8")]
157    Utf8Error(#[from] std::str::Utf8Error),
158
159    /// An attempt was made to modify a readonly variable.
160    #[error("cannot mutate readonly variable")]
161    ReadonlyVariable,
162
163    /// The indicated pattern is invalid.
164    #[error("invalid pattern: '{0}'")]
165    InvalidPattern(String),
166
167    /// A regular expression error occurred
168    #[error("regex error: {0}")]
169    RegexError(#[from] fancy_regex::Error),
170
171    /// An invalid regular expression was provided.
172    #[error("invalid regex: {0}; expression: '{1}'")]
173    InvalidRegexError(fancy_regex::Error, String),
174
175    /// An I/O error occurred.
176    #[error("i/o error: {0}")]
177    IoError(#[from] std::io::Error),
178
179    /// Invalid substitution syntax.
180    #[error("bad substitution: {0}")]
181    BadSubstitution(String),
182
183    /// An error occurred while creating a child process.
184    #[error("failed to create child process")]
185    ChildCreationFailure,
186
187    /// An error occurred while formatting a string.
188    #[error(transparent)]
189    FormattingError(#[from] std::fmt::Error),
190
191    /// An error occurred while parsing.
192    #[error("{1}: {0}")]
193    ParseError(crate::parser::ParseError, crate::SourceInfo),
194
195    /// An error occurred while parsing a function body.
196    #[error("{0}: {1}")]
197    FunctionParseError(String, crate::parser::ParseError),
198
199    /// An error occurred while parsing a word.
200    #[error(transparent)]
201    WordParseError(#[from] crate::parser::WordParseError),
202
203    /// Unable to parse a test command.
204    #[error("invalid test command")]
205    TestCommandParseError(#[from] crate::parser::TestCommandParseError),
206
207    /// Unable to parse a key binding specification.
208    #[error(transparent)]
209    BindingParseError(#[from] crate::parser::BindingParseError),
210
211    /// A threading error occurred.
212    #[error("threading error")]
213    ThreadingError(#[from] tokio::task::JoinError),
214
215    /// An invalid signal was referenced.
216    #[error("{0}: invalid signal specification")]
217    InvalidSignal(String),
218
219    /// A platform error occurred.
220    #[error("platform error: {0}")]
221    PlatformError(#[from] sys::PlatformError),
222
223    /// An invalid umask was provided.
224    #[error("invalid umask value")]
225    InvalidUmask,
226
227    /// The given open file cannot be read from.
228    #[error("cannot read from {0}")]
229    OpenFileNotReadable(&'static str),
230
231    /// The given open file cannot be written to.
232    #[error("cannot write to {0}")]
233    OpenFileNotWritable(&'static str),
234
235    /// Bad file descriptor.
236    #[error("bad file descriptor: {0}")]
237    BadFileDescriptor(ShellFd),
238
239    /// Printf failure
240    #[error("printf failure: {0}")]
241    PrintfFailure(i32),
242
243    /// Printf invalid usage
244    #[error("printf: {0}")]
245    PrintfInvalidUsage(String),
246
247    /// Interrupted
248    #[error("interrupted")]
249    Interrupted,
250
251    /// Maximum function call depth was exceeded.
252    #[error("maximum function call depth exceeded")]
253    MaxFunctionCallDepthExceeded,
254
255    /// System time error.
256    #[error("system time error: {0}")]
257    TimeError(#[from] std::time::SystemTimeError),
258
259    /// Array index out of range.
260    #[error("array index out of range: {0}")]
261    ArrayIndexOutOfRange(String),
262
263    /// Unhandled key code.
264    #[error("unhandled key code: {0:?}")]
265    UnhandledKeyCode(Vec<u8>),
266
267    /// An error occurred in a built-in command.
268    #[error("{1}: {0}")]
269    BuiltinError(Box<dyn BuiltinError>, String),
270
271    /// Operation not supported on this platform.
272    #[error("operation not supported on this platform: {0}")]
273    NotSupportedOnThisPlatform(&'static str),
274
275    /// Command history is not enabled in this shell.
276    #[error("command history is not enabled in this shell")]
277    HistoryNotEnabled,
278
279    /// Expanding an unset variable.
280    #[error("expanding unset variable: {0}")]
281    ExpandingUnsetVariable(String),
282
283    /// An internal error occurred.
284    #[error("internal shell error: {0}")]
285    InternalError(String),
286
287    /// Attempted to perform an operation that requires an interactive session.
288    #[error("operation requires an interactive session")]
289    NotInInteractiveSession,
290
291    /// Attempted to perform an operation that requires command-string mode.
292    #[error("operation requires command-string mode")]
293    NotExecutingCommandString,
294
295    /// Too much data was provided to an operation.
296    #[error("too much data")]
297    TooMuchData,
298
299    /// Cannot convert open file to native file descriptor.
300    #[error("cannot convert open file to native file descriptor")]
301    CannotConvertToNativeFd,
302
303    /// History file is too large to import.
304    #[error("history file is too large to import")]
305    HistoryFileTooLargeToImport,
306
307    /// Too many open files.
308    #[error("too many open files")]
309    TooManyOpenFiles,
310
311    /// The function name shadows a special built-in command.
312    #[error("function name '{}' shadows a special built-in command", .name)]
313    FunctionNameShadowsSpecialBuiltin {
314        /// Name of the function.
315        name: String,
316    },
317
318    /// A glob pattern failed to match any files (failglob).
319    #[error("no match: {0}")]
320    NoMatch(String),
321}
322
323/// Trait implementable by built-in commands to represent errors.
324pub trait BuiltinError: std::error::Error + ConvertibleToExitCode + Send + Sync {
325    /// Try to extract a reference to the underlying `std::io::Error`, if any.
326    /// Implementations should return `None` if there is no inner I/O error.
327    /// They should not attempt to *synthesize* an I/O error if one does not
328    /// naturally exist.
329    fn as_io_error(&self) -> Option<&std::io::Error> {
330        None
331    }
332}
333
334impl BuiltinError for Error {
335    fn as_io_error(&self) -> Option<&std::io::Error> {
336        self.as_io_error()
337    }
338}
339
340/// Helper trait for converting values to exit codes.
341pub trait ConvertibleToExitCode {
342    /// Converts to an exit code.
343    fn as_exit_code(&self) -> results::ExecutionExitCode;
344}
345
346impl<T> ConvertibleToExitCode for T
347where
348    results::ExecutionExitCode: for<'a> From<&'a T>,
349{
350    fn as_exit_code(&self) -> results::ExecutionExitCode {
351        self.into()
352    }
353}
354
355impl From<&ErrorKind> for results::ExecutionExitCode {
356    fn from(value: &ErrorKind) -> Self {
357        match value {
358            ErrorKind::CommandNotFound(..) => Self::NotFound,
359            ErrorKind::Unimplemented(..) | ErrorKind::UnimplementedAndTracked(..) => {
360                Self::Unimplemented
361            }
362            ErrorKind::ParseError(..) => Self::InvalidUsage,
363            ErrorKind::FunctionParseError(..) => Self::InvalidUsage,
364            ErrorKind::TestCommandParseError(..) => Self::InvalidUsage,
365            ErrorKind::FailedToExecuteCommand(..) => Self::CannotExecute,
366            ErrorKind::FunctionNameShadowsSpecialBuiltin { .. } => Self::InvalidUsage,
367            ErrorKind::IoError(io_err) => io_err.into(),
368            ErrorKind::BuiltinError(inner, ..) => inner.as_exit_code(),
369            _ => Self::GeneralError,
370        }
371    }
372}
373
374impl From<&std::io::Error> for results::ExecutionExitCode {
375    fn from(io_err: &std::io::Error) -> Self {
376        if io_err.kind() == std::io::ErrorKind::BrokenPipe {
377            Self::BrokenPipe
378        } else {
379            Self::GeneralError
380        }
381    }
382}
383
384impl From<&Error> for results::ExecutionExitCode {
385    fn from(error: &Error) -> Self {
386        Self::from(&error.kind)
387    }
388}
389
390impl<T> From<T> for Error
391where
392    ErrorKind: From<T>,
393{
394    fn from(convertible_to_kind: T) -> Self {
395        Self {
396            kind: convertible_to_kind.into(),
397            fatal: false,
398        }
399    }
400}
401
402impl Error {
403    /// Marks this error as fatal.
404    #[must_use]
405    pub const fn into_fatal(mut self) -> Self {
406        self.fatal = true;
407        self
408    }
409
410    /// Returns whether or not this error is fatal.
411    pub const fn is_fatal(&self) -> bool {
412        self.fatal
413    }
414
415    /// Returns a reference to the error kind.
416    pub const fn kind(&self) -> &ErrorKind {
417        &self.kind
418    }
419
420    /// Try to extract a reference to the underlying `std::io::Error`, if any.
421    pub fn as_io_error(&self) -> Option<&std::io::Error> {
422        match &self.kind {
423            ErrorKind::IoError(io_err) => Some(io_err),
424            ErrorKind::BuiltinError(inner, _) => inner.as_io_error(),
425            _ => None,
426        }
427    }
428
429    /// Converts this error into the appropriate control flow based on the shell's current state.
430    /// This centralizes the logic for determining how fatal errors should affect execution flow.
431    ///
432    /// # Arguments
433    ///
434    /// * `shell` - The shell instance, used to check interactive mode and script call stack.
435    pub fn to_control_flow(
436        &self,
437        shell: &Shell<impl extensions::ShellExtensions>,
438    ) -> results::ExecutionControlFlow {
439        if self.is_fatal() && !shell.options().interactive {
440            results::ExecutionControlFlow::ExitShell
441        } else {
442            results::ExecutionControlFlow::Normal
443        }
444    }
445
446    /// Converts this error into an execution result for the shell.
447    ///
448    /// # Arguments
449    ///
450    /// * `shell` - The shell instance, used to determine control flow.
451    pub fn into_result(
452        self,
453        shell: &Shell<impl extensions::ShellExtensions>,
454    ) -> results::ExecutionResult {
455        let next_control_flow = self.to_control_flow(shell);
456        let exit_code = results::ExecutionExitCode::from(&self);
457
458        results::ExecutionResult {
459            next_control_flow,
460            exit_code,
461        }
462    }
463}
464
465/// Convenience function for returning an error for unimplemented functionality.
466///
467/// # Arguments
468///
469/// * `msg` - The message to include in the error
470pub fn unimp<T>(msg: &'static str) -> Result<T, Error> {
471    Err(ErrorKind::Unimplemented(msg).into())
472}
473
474/// Convenience function for returning an error for *tracked*, unimplemented functionality.
475///
476/// # Arguments
477///
478/// * `msg` - The message to include in the error
479/// * `project_issue_id` - The GitHub issue ID where the implementation is tracked.
480pub fn unimp_with_issue<T>(msg: &'static str, project_issue_id: u32) -> Result<T, Error> {
481    Err(ErrorKind::UnimplementedAndTracked(msg, project_issue_id).into())
482}