brush_core/
error.rs

1//! Error facilities
2
3use std::path::PathBuf;
4
5use crate::{Shell, ShellFd, 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(transparent)]
11pub struct Error {
12    /// The kind of error.
13    kind: ErrorKind,
14}
15
16/// Monolithic error type for the shell
17#[derive(thiserror::Error, Debug)]
18pub enum ErrorKind {
19    /// A tilde expression was used without a valid HOME variable
20    #[error("cannot expand tilde expression with HOME not set")]
21    TildeWithoutValidHome,
22
23    /// An attempt was made to assign a list to an array member
24    #[error("cannot assign list to array member")]
25    AssigningListToArrayMember,
26
27    /// An attempt was made to convert an associative array to an indexed array.
28    #[error("cannot convert associative array to indexed array")]
29    ConvertingAssociativeArrayToIndexedArray,
30
31    /// An attempt was made to convert an indexed array to an associative array.
32    #[error("cannot convert indexed array to associative array")]
33    ConvertingIndexedArrayToAssociativeArray,
34
35    /// An error occurred while sourcing the indicated script file.
36    #[error("failed to source file: {0}")]
37    FailedSourcingFile(PathBuf, #[source] std::io::Error),
38
39    /// The shell failed to send a signal to a process.
40    #[error("failed to send signal to process")]
41    FailedToSendSignal,
42
43    /// An attempt was made to assign a value to a special parameter.
44    #[error("cannot assign in this way")]
45    CannotAssignToSpecialParameter,
46
47    /// Checked expansion error.
48    #[error("expansion error: {0}")]
49    CheckedExpansionError(String),
50
51    /// A reference was made to an unknown shell function.
52    #[error("function not found: {0}")]
53    FunctionNotFound(String),
54
55    /// Command was not found.
56    #[error("command not found: {0}")]
57    CommandNotFound(String),
58
59    /// Not a builtin.
60    #[error("not a shell builtin: {0}")]
61    BuiltinNotFound(String),
62
63    /// The working directory does not exist.
64    #[error("working directory does not exist: {0}")]
65    WorkingDirMissing(PathBuf),
66
67    /// Failed to execute command.
68    #[error("failed to execute command '{0}': {1}")]
69    FailedToExecuteCommand(String, #[source] std::io::Error),
70
71    /// History item was not found.
72    #[error("history item not found")]
73    HistoryItemNotFound,
74
75    /// The requested functionality has not yet been implemented in this shell.
76    #[error("not yet implemented: {0}")]
77    Unimplemented(&'static str),
78
79    /// The requested functionality has not yet been implemented in this shell; it is tracked in a
80    /// GitHub issue.
81    #[error("not yet implemented: {0}; see https://github.com/reubeno/brush/issues/{1}")]
82    UnimplementedAndTracked(&'static str, u32),
83
84    /// An expected environment scope could not be found.
85    #[error("missing scope")]
86    MissingScope,
87
88    /// The given path is not a directory.
89    #[error("not a directory: {0}")]
90    NotADirectory(PathBuf),
91
92    /// The given path is a directory.
93    #[error("path is a directory")]
94    IsADirectory,
95
96    /// The given variable is not an array.
97    #[error("variable is not an array")]
98    NotArray,
99
100    /// The current user could not be determined.
101    #[error("no current user")]
102    NoCurrentUser,
103
104    /// The requested input or output redirection is invalid.
105    #[error("invalid redirection")]
106    InvalidRedirection,
107
108    /// An error occurred while redirecting input or output with the given file.
109    #[error("failed to redirect to {0}: {1}")]
110    RedirectionFailure(String, String),
111
112    /// An error occurred evaluating an arithmetic expression.
113    #[error("arithmetic evaluation error: {0}")]
114    EvalError(#[from] crate::arithmetic::EvalError),
115
116    /// The given string could not be parsed as an integer.
117    #[error("failed to parse integer")]
118    IntParseError(#[from] std::num::ParseIntError),
119
120    /// The given string could not be parsed as an integer.
121    #[error("failed to parse integer")]
122    TryIntParseError(#[from] std::num::TryFromIntError),
123
124    /// A byte sequence could not be decoded as a valid UTF-8 string.
125    #[error("failed to decode utf-8")]
126    FromUtf8Error(#[from] std::string::FromUtf8Error),
127
128    /// A byte sequence could not be decoded as a valid UTF-8 string.
129    #[error("failed to decode utf-8")]
130    Utf8Error(#[from] std::str::Utf8Error),
131
132    /// An attempt was made to modify a readonly variable.
133    #[error("cannot mutate readonly variable")]
134    ReadonlyVariable,
135
136    /// The indicated pattern is invalid.
137    #[error("invalid pattern: '{0}'")]
138    InvalidPattern(String),
139
140    /// A regular expression error occurred
141    #[error("regex error: {0}")]
142    RegexError(#[from] fancy_regex::Error),
143
144    /// An invalid regular expression was provided.
145    #[error("invalid regex: {0}; expression: '{1}'")]
146    InvalidRegexError(fancy_regex::Error, String),
147
148    /// An I/O error occurred.
149    #[error("i/o error: {0}")]
150    IoError(#[from] std::io::Error),
151
152    /// Invalid substitution syntax.
153    #[error("bad substitution: {0}")]
154    BadSubstitution(String),
155
156    /// An error occurred while creating a child process.
157    #[error("failed to create child process")]
158    ChildCreationFailure,
159
160    /// An error occurred while formatting a string.
161    #[error(transparent)]
162    FormattingError(#[from] std::fmt::Error),
163
164    /// An error occurred while parsing.
165    #[error("{1}: {0}")]
166    ParseError(brush_parser::ParseError, brush_parser::SourceInfo),
167
168    /// An error occurred while parsing a function body.
169    #[error("{0}: {1}")]
170    FunctionParseError(String, brush_parser::ParseError),
171
172    /// An error occurred while parsing a word.
173    #[error(transparent)]
174    WordParseError(#[from] brush_parser::WordParseError),
175
176    /// Unable to parse a test command.
177    #[error(transparent)]
178    TestCommandParseError(#[from] brush_parser::TestCommandParseError),
179
180    /// Unable to parse a key binding specification.
181    #[error(transparent)]
182    BindingParseError(#[from] brush_parser::BindingParseError),
183
184    /// A threading error occurred.
185    #[error("threading error")]
186    ThreadingError(#[from] tokio::task::JoinError),
187
188    /// An invalid signal was referenced.
189    #[error("{0}: invalid signal specification")]
190    InvalidSignal(String),
191
192    /// A platform error occurred.
193    #[error("platform error: {0}")]
194    PlatformError(#[from] sys::PlatformError),
195
196    /// An invalid umask was provided.
197    #[error("invalid umask value")]
198    InvalidUmask,
199
200    /// The given open file cannot be read from.
201    #[error("cannot read from {0}")]
202    OpenFileNotReadable(&'static str),
203
204    /// The given open file cannot be written to.
205    #[error("cannot write to {0}")]
206    OpenFileNotWritable(&'static str),
207
208    /// Bad file descriptor.
209    #[error("bad file descriptor: {0}")]
210    BadFileDescriptor(ShellFd),
211
212    /// Printf failure
213    #[error("printf failure: {0}")]
214    PrintfFailure(i32),
215
216    /// Printf invalid usage
217    #[error("printf: {0}")]
218    PrintfInvalidUsage(String),
219
220    /// Interrupted
221    #[error("interrupted")]
222    Interrupted,
223
224    /// Maximum function call depth was exceeded.
225    #[error("maximum function call depth exceeded")]
226    MaxFunctionCallDepthExceeded,
227
228    /// System time error.
229    #[error("system time error: {0}")]
230    TimeError(#[from] std::time::SystemTimeError),
231
232    /// Array index out of range.
233    #[error("array index out of range: {0}")]
234    ArrayIndexOutOfRange(i64),
235
236    /// Unhandled key code.
237    #[error("unhandled key code: {0:?}")]
238    UnhandledKeyCode(Vec<u8>),
239
240    /// An error occurred in a built-in command.
241    #[error("{1}: {0}")]
242    BuiltinError(Box<dyn BuiltinError>, String),
243
244    /// Operation not supported on this platform.
245    #[error("operation not supported on this platform: {0}")]
246    NotSupportedOnThisPlatform(&'static str),
247
248    /// Command history is not enabled in this shell.
249    #[error("command history is not enabled in this shell")]
250    HistoryNotEnabled,
251
252    /// Unknown key binding function.
253    #[error("unknown key binding function: {0}")]
254    UnknownKeyBindingFunction(String),
255}
256
257impl BuiltinError for Error {}
258
259/// Trait implementable by built-in commands to represent errors.
260pub trait BuiltinError: std::error::Error + ConvertibleToExitCode + Send + Sync {}
261
262/// Helper trait for converting values to exit codes.
263pub trait ConvertibleToExitCode {
264    /// Converts to an exit code.
265    fn as_exit_code(&self) -> results::ExecutionExitCode;
266}
267
268impl<T> ConvertibleToExitCode for T
269where
270    results::ExecutionExitCode: for<'a> From<&'a T>,
271{
272    fn as_exit_code(&self) -> results::ExecutionExitCode {
273        self.into()
274    }
275}
276
277impl From<&ErrorKind> for results::ExecutionExitCode {
278    fn from(value: &ErrorKind) -> Self {
279        match value {
280            ErrorKind::CommandNotFound(..) => Self::NotFound,
281            ErrorKind::Unimplemented(..) | ErrorKind::UnimplementedAndTracked(..) => {
282                Self::Unimplemented
283            }
284            ErrorKind::ParseError(..) => Self::InvalidUsage,
285            ErrorKind::FunctionParseError(..) => Self::InvalidUsage,
286            ErrorKind::FailedToExecuteCommand(..) => Self::CannotExecute,
287            ErrorKind::BuiltinError(inner, ..) => inner.as_exit_code(),
288            _ => Self::GeneralError,
289        }
290    }
291}
292
293impl From<&Error> for results::ExecutionExitCode {
294    fn from(error: &Error) -> Self {
295        Self::from(&error.kind)
296    }
297}
298
299impl<T> From<T> for Error
300where
301    ErrorKind: From<T>,
302{
303    fn from(convertible_to_kind: T) -> Self {
304        Self {
305            kind: convertible_to_kind.into(),
306        }
307    }
308}
309
310/// Trait implementable by consumers of this crate to customize formatting errors into
311/// displayable text.
312pub trait ErrorFormatter: Send {
313    /// Format the given error for display within the context of the provided shell.
314    ///
315    /// # Arguments
316    ///
317    /// * `error` - The error to format.
318    /// * `shell` - The shell in which the error occurred.
319    fn format_error(&self, error: &Error, shell: &Shell) -> String;
320}
321
322/// Default implementation of the [`ErrorFormatter`] trait.
323pub(crate) struct DefaultErrorFormatter {}
324
325impl DefaultErrorFormatter {
326    pub const fn new() -> Self {
327        Self {}
328    }
329}
330
331impl ErrorFormatter for DefaultErrorFormatter {
332    fn format_error(&self, err: &Error, _shell: &Shell) -> String {
333        std::format!("error: {err:#}\n")
334    }
335}
336
337/// Convenience function for returning an error for unimplemented functionality.
338///
339/// # Arguments
340///
341/// * `msg` - The message to include in the error
342pub fn unimp<T>(msg: &'static str) -> Result<T, Error> {
343    Err(ErrorKind::Unimplemented(msg).into())
344}
345
346/// Convenience function for returning an error for *tracked*, unimplemented functionality.
347///
348/// # Arguments
349///
350/// * `msg` - The message to include in the error
351/// * `project_issue_id` - The GitHub issue ID where the implementation is tracked.
352#[allow(unused)]
353pub fn unimp_with_issue<T>(msg: &'static str, project_issue_id: u32) -> Result<T, Error> {
354    Err(ErrorKind::UnimplementedAndTracked(msg, project_issue_id).into())
355}