Skip to main content

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