483 lines
15 KiB
Rust
483 lines
15 KiB
Rust
//! Error facilities
|
|||
|
|
|
||
|
|
use std::path::PathBuf;
|
||
|
|
|
||
|
|
use crate::{Shell, ShellFd, extensions, results, sys};
|
||
|
|
|
||
|
|
/// Unified error type for this crate. Contains just a kind for now,
|
||
|
|
/// but will be extended later with additional context.
|
||
|
|
#[derive(thiserror::Error, Debug)]
|
||
|
|
#[error("{kind}")]
|
||
|
|
pub struct Error {
|
||
|
|
/// The kind of error.
|
||
|
|
#[source]
|
||
|
|
kind: ErrorKind,
|
||
|
|
|
||
|
|
/// Whether or not the error should be considered a "fatal" error that would
|
||
|
|
/// result in abnormal exit of a non-interactive shell.
|
||
|
|
fatal: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Monolithic error type for the shell
|
||
|
|
#[derive(thiserror::Error, Debug)]
|
||
|
|
pub enum ErrorKind {
|
||
|
|
/// A tilde expression was used without a valid HOME variable
|
||
|
|
#[error("cannot expand tilde expression with HOME not set")]
|
||
|
|
TildeWithoutValidHome,
|
||
|
|
|
||
|
|
/// An attempt was made to assign a list to an array member
|
||
|
|
#[error("cannot assign list to array member")]
|
||
|
|
AssigningListToArrayMember,
|
||
|
|
|
||
|
|
/// An attempt was made to convert an associative array to an indexed array.
|
||
|
|
#[error("cannot convert associative array to indexed array")]
|
||
|
|
ConvertingAssociativeArrayToIndexedArray,
|
||
|
|
|
||
|
|
/// An attempt was made to convert an indexed array to an associative array.
|
||
|
|
#[error("cannot convert indexed array to associative array")]
|
||
|
|
ConvertingIndexedArrayToAssociativeArray,
|
||
|
|
|
||
|
|
/// An error occurred while sourcing the indicated script file.
|
||
|
|
#[error("failed to source file: {0}")]
|
||
|
|
FailedSourcingFile(PathBuf, #[source] std::io::Error),
|
||
|
|
|
||
|
|
/// The shell failed to send a signal to a process.
|
||
|
|
#[error("failed to send signal to process")]
|
||
|
|
FailedToSendSignal,
|
||
|
|
|
||
|
|
/// An attempt was made to assign a value to a special parameter.
|
||
|
|
#[error("cannot assign in this way")]
|
||
|
|
CannotAssignToSpecialParameter,
|
||
|
|
|
||
|
|
/// Checked expansion error.
|
||
|
|
#[error("expansion error: {0}")]
|
||
|
|
CheckedExpansionError(String),
|
||
|
|
|
||
|
|
/// A reference was made to an unknown shell function.
|
||
|
|
#[error("function not found: {0}")]
|
||
|
|
FunctionNotFound(String),
|
||
|
|
|
||
|
|
/// Command was not found.
|
||
|
|
#[error("command not found: {0}")]
|
||
|
|
CommandNotFound(String),
|
||
|
|
|
||
|
|
/// Not a builtin.
|
||
|
|
#[error("not a shell builtin: {0}")]
|
||
|
|
BuiltinNotFound(String),
|
||
|
|
|
||
|
|
/// The working directory does not exist.
|
||
|
|
#[error("working directory does not exist: {0}")]
|
||
|
|
WorkingDirMissing(PathBuf),
|
||
|
|
|
||
|
|
/// Failed to execute command.
|
||
|
|
#[error("failed to execute command '{0}': {1}")]
|
||
|
|
FailedToExecuteCommand(String, #[source] std::io::Error),
|
||
|
|
|
||
|
|
/// History item was not found.
|
||
|
|
#[error("history item not found")]
|
||
|
|
HistoryItemNotFound,
|
||
|
|
|
||
|
|
/// The requested functionality has not yet been implemented in this shell.
|
||
|
|
#[error("not yet implemented: {0}")]
|
||
|
|
Unimplemented(&'static str),
|
||
|
|
|
||
|
|
/// The requested functionality has not yet been implemented in this shell; it is tracked in a
|
||
|
|
/// GitHub issue.
|
||
|
|
#[error("not yet implemented: {0}; see https://github.com/reubeno/brush/issues/{1}")]
|
||
|
|
UnimplementedAndTracked(&'static str, u32),
|
||
|
|
|
||
|
|
/// An expected environment scope could not be found.
|
||
|
|
#[error("missing environment scope")]
|
||
|
|
MissingScope,
|
||
|
|
|
||
|
|
/// The environment scope required for a new variable is not available.
|
||
|
|
#[error("environment scope required for new variable is not available")]
|
||
|
|
MissingScopeForNewVariable,
|
||
|
|
|
||
|
|
/// An unexpected environment scope type was encountered.
|
||
|
|
#[error("unexpected environment scope type: expected '{expected}', found '{actual}'")]
|
||
|
|
UnexpectedScopeType {
|
||
|
|
/// The expected scope type.
|
||
|
|
expected: crate::env::EnvironmentScope,
|
||
|
|
/// The actual scope type.
|
||
|
|
actual: crate::env::EnvironmentScope,
|
||
|
|
},
|
||
|
|
|
||
|
|
/// The given path is not a directory.
|
||
|
|
#[error("not a directory: {0}")]
|
||
|
|
NotADirectory(PathBuf),
|
||
|
|
|
||
|
|
/// The given path is a directory.
|
||
|
|
#[error("path is a directory")]
|
||
|
|
IsADirectory,
|
||
|
|
|
||
|
|
/// The given variable is not an array.
|
||
|
|
#[error("variable is not an array")]
|
||
|
|
NotArray,
|
||
|
|
|
||
|
|
/// The current user could not be determined.
|
||
|
|
#[error("no current user")]
|
||
|
|
NoCurrentUser,
|
||
|
|
|
||
|
|
/// The requested input or output redirection is invalid.
|
||
|
|
#[error("invalid redirection target")]
|
||
|
|
InvalidRedirection,
|
||
|
|
|
||
|
|
/// An error occurred while redirecting input or output with the given file.
|
||
|
|
#[error("failed to redirect to {0}: {1}")]
|
||
|
|
RedirectionFailure(String, String),
|
||
|
|
|
||
|
|
/// An error occurred evaluating an arithmetic expression.
|
||
|
|
#[error("arithmetic evaluation error: {0}")]
|
||
|
|
EvalError(#[from] crate::arithmetic::EvalError),
|
||
|
|
|
||
|
|
/// The given string could not be parsed as an integer.
|
||
|
|
#[error("failed to parse '{s}' as a {int_type_name}, base-{radix} integer: {inner}")]
|
||
|
|
IntParseError {
|
||
|
|
/// The string that failed to parse.
|
||
|
|
s: String,
|
||
|
|
/// The integer type being parsed.
|
||
|
|
int_type_name: &'static str,
|
||
|
|
/// The radix (base) used for parsing.
|
||
|
|
radix: u32,
|
||
|
|
/// The underlying parse error.
|
||
|
|
inner: std::num::ParseIntError,
|
||
|
|
},
|
||
|
|
|
||
|
|
/// The given integer could not be converted to the target type.
|
||
|
|
#[error("integer conversion error")]
|
||
|
|
TryIntParseError(#[from] std::num::TryFromIntError),
|
||
|
|
|
||
|
|
/// A byte sequence could not be decoded as a valid UTF-8 string.
|
||
|
|
#[error("failed to decode utf-8")]
|
||
|
|
FromUtf8Error(#[from] std::string::FromUtf8Error),
|
||
|
|
|
||
|
|
/// A byte sequence could not be decoded as a valid UTF-8 string.
|
||
|
|
#[error("failed to decode utf-8")]
|
||
|
|
Utf8Error(#[from] std::str::Utf8Error),
|
||
|
|
|
||
|
|
/// An attempt was made to modify a readonly variable.
|
||
|
|
#[error("cannot mutate readonly variable")]
|
||
|
|
ReadonlyVariable,
|
||
|
|
|
||
|
|
/// The indicated pattern is invalid.
|
||
|
|
#[error("invalid pattern: '{0}'")]
|
||
|
|
InvalidPattern(String),
|
||
|
|
|
||
|
|
/// A regular expression error occurred
|
||
|
|
#[error("regex error: {0}")]
|
||
|
|
RegexError(#[from] fancy_regex::Error),
|
||
|
|
|
||
|
|
/// An invalid regular expression was provided.
|
||
|
|
#[error("invalid regex: {0}; expression: '{1}'")]
|
||
|
|
InvalidRegexError(fancy_regex::Error, String),
|
||
|
|
|
||
|
|
/// An I/O error occurred.
|
||
|
|
#[error("i/o error: {0}")]
|
||
|
|
IoError(#[from] std::io::Error),
|
||
|
|
|
||
|
|
/// Invalid substitution syntax.
|
||
|
|
#[error("bad substitution: {0}")]
|
||
|
|
BadSubstitution(String),
|
||
|
|
|
||
|
|
/// An error occurred while creating a child process.
|
||
|
|
#[error("failed to create child process")]
|
||
|
|
ChildCreationFailure,
|
||
|
|
|
||
|
|
/// An error occurred while formatting a string.
|
||
|
|
#[error(transparent)]
|
||
|
|
FormattingError(#[from] std::fmt::Error),
|
||
|
|
|
||
|
|
/// An error occurred while parsing.
|
||
|
|
#[error("{1}: {0}")]
|
||
|
|
ParseError(crate::parser::ParseError, crate::SourceInfo),
|
||
|
|
|
||
|
|
/// An error occurred while parsing a function body.
|
||
|
|
#[error("{0}: {1}")]
|
||
|
|
FunctionParseError(String, crate::parser::ParseError),
|
||
|
|
|
||
|
|
/// An error occurred while parsing a word.
|
||
|
|
#[error(transparent)]
|
||
|
|
WordParseError(#[from] crate::parser::WordParseError),
|
||
|
|
|
||
|
|
/// Unable to parse a test command.
|
||
|
|
#[error("invalid test command")]
|
||
|
|
TestCommandParseError(#[from] crate::parser::TestCommandParseError),
|
||
|
|
|
||
|
|
/// Unable to parse a key binding specification.
|
||
|
|
#[error(transparent)]
|
||
|
|
BindingParseError(#[from] crate::parser::BindingParseError),
|
||
|
|
|
||
|
|
/// A threading error occurred.
|
||
|
|
#[error("threading error")]
|
||
|
|
ThreadingError(#[from] tokio::task::JoinError),
|
||
|
|
|
||
|
|
/// An invalid signal was referenced.
|
||
|
|
#[error("{0}: invalid signal specification")]
|
||
|
|
InvalidSignal(String),
|
||
|
|
|
||
|
|
/// A platform error occurred.
|
||
|
|
#[error("platform error: {0}")]
|
||
|
|
PlatformError(#[from] sys::PlatformError),
|
||
|
|
|
||
|
|
/// An invalid umask was provided.
|
||
|
|
#[error("invalid umask value")]
|
||
|
|
InvalidUmask,
|
||
|
|
|
||
|
|
/// The given open file cannot be read from.
|
||
|
|
#[error("cannot read from {0}")]
|
||
|
|
OpenFileNotReadable(&'static str),
|
||
|
|
|
||
|
|
/// The given open file cannot be written to.
|
||
|
|
#[error("cannot write to {0}")]
|
||
|
|
OpenFileNotWritable(&'static str),
|
||
|
|
|
||
|
|
/// Bad file descriptor.
|
||
|
|
#[error("bad file descriptor: {0}")]
|
||
|
|
BadFileDescriptor(ShellFd),
|
||
|
|
|
||
|
|
/// Printf failure
|
||
|
|
#[error("printf failure: {0}")]
|
||
|
|
PrintfFailure(i32),
|
||
|
|
|
||
|
|
/// Printf invalid usage
|
||
|
|
#[error("printf: {0}")]
|
||
|
|
PrintfInvalidUsage(String),
|
||
|
|
|
||
|
|
/// Interrupted
|
||
|
|
#[error("interrupted")]
|
||
|
|
Interrupted,
|
||
|
|
|
||
|
|
/// Maximum function call depth was exceeded.
|
||
|
|
#[error("maximum function call depth exceeded")]
|
||
|
|
MaxFunctionCallDepthExceeded,
|
||
|
|
|
||
|
|
/// System time error.
|
||
|
|
#[error("system time error: {0}")]
|
||
|
|
TimeError(#[from] std::time::SystemTimeError),
|
||
|
|
|
||
|
|
/// Array index out of range.
|
||
|
|
#[error("array index out of range: {0}")]
|
||
|
|
ArrayIndexOutOfRange(String),
|
||
|
|
|
||
|
|
/// Unhandled key code.
|
||
|
|
#[error("unhandled key code: {0:?}")]
|
||
|
|
UnhandledKeyCode(Vec<u8>),
|
||
|
|
|
||
|
|
/// An error occurred in a built-in command.
|
||
|
|
#[error("{1}: {0}")]
|
||
|
|
BuiltinError(Box<dyn BuiltinError>, String),
|
||
|
|
|
||
|
|
/// Operation not supported on this platform.
|
||
|
|
#[error("operation not supported on this platform: {0}")]
|
||
|
|
NotSupportedOnThisPlatform(&'static str),
|
||
|
|
|
||
|
|
/// Command history is not enabled in this shell.
|
||
|
|
#[error("command history is not enabled in this shell")]
|
||
|
|
HistoryNotEnabled,
|
||
|
|
|
||
|
|
/// Expanding an unset variable.
|
||
|
|
#[error("expanding unset variable: {0}")]
|
||
|
|
ExpandingUnsetVariable(String),
|
||
|
|
|
||
|
|
/// An internal error occurred.
|
||
|
|
#[error("internal shell error: {0}")]
|
||
|
|
InternalError(String),
|
||
|
|
|
||
|
|
/// Attempted to perform an operation that requires an interactive session.
|
||
|
|
#[error("operation requires an interactive session")]
|
||
|
|
NotInInteractiveSession,
|
||
|
|
|
||
|
|
/// Attempted to perform an operation that requires command-string mode.
|
||
|
|
#[error("operation requires command-string mode")]
|
||
|
|
NotExecutingCommandString,
|
||
|
|
|
||
|
|
/// Too much data was provided to an operation.
|
||
|
|
#[error("too much data")]
|
||
|
|
TooMuchData,
|
||
|
|
|
||
|
|
/// Cannot convert open file to native file descriptor.
|
||
|
|
#[error("cannot convert open file to native file descriptor")]
|
||
|
|
CannotConvertToNativeFd,
|
||
|
|
|
||
|
|
/// History file is too large to import.
|
||
|
|
#[error("history file is too large to import")]
|
||
|
|
HistoryFileTooLargeToImport,
|
||
|
|
|
||
|
|
/// Too many open files.
|
||
|
|
#[error("too many open files")]
|
||
|
|
TooManyOpenFiles,
|
||
|
|
|
||
|
|
/// The function name shadows a special built-in command.
|
||
|
|
#[error("function name '{}' shadows a special built-in command", .name)]
|
||
|
|
FunctionNameShadowsSpecialBuiltin {
|
||
|
|
/// Name of the function.
|
||
|
|
name: String,
|
||
|
|
},
|
||
|
|
|
||
|
|
/// A glob pattern failed to match any files (failglob).
|
||
|
|
#[error("no match: {0}")]
|
||
|
|
NoMatch(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trait implementable by built-in commands to represent errors.
|
||
|
|
pub trait BuiltinError: std::error::Error + ConvertibleToExitCode + Send + Sync {
|
||
|
|
/// Try to extract a reference to the underlying `std::io::Error`, if any.
|
||
|
|
/// Implementations should return `None` if there is no inner I/O error.
|
||
|
|
/// They should not attempt to *synthesize* an I/O error if one does not
|
||
|
|
/// naturally exist.
|
||
|
|
fn as_io_error(&self) -> Option<&std::io::Error> {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl BuiltinError for Error {
|
||
|
|
fn as_io_error(&self) -> Option<&std::io::Error> {
|
||
|
|
self.as_io_error()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper trait for converting values to exit codes.
|
||
|
|
pub trait ConvertibleToExitCode {
|
||
|
|
/// Converts to an exit code.
|
||
|
|
fn as_exit_code(&self) -> results::ExecutionExitCode;
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<T> ConvertibleToExitCode for T
|
||
|
|
where
|
||
|
|
results::ExecutionExitCode: for<'a> From<&'a T>,
|
||
|
|
{
|
||
|
|
fn as_exit_code(&self) -> results::ExecutionExitCode {
|
||
|
|
self.into()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<&ErrorKind> for results::ExecutionExitCode {
|
||
|
|
fn from(value: &ErrorKind) -> Self {
|
||
|
|
match value {
|
||
|
|
ErrorKind::CommandNotFound(..) => Self::NotFound,
|
||
|
|
ErrorKind::Unimplemented(..) | ErrorKind::UnimplementedAndTracked(..) => {
|
||
|
|
Self::Unimplemented
|
||
|
|
}
|
||
|
|
ErrorKind::ParseError(..) => Self::InvalidUsage,
|
||
|
|
ErrorKind::FunctionParseError(..) => Self::InvalidUsage,
|
||
|
|
ErrorKind::TestCommandParseError(..) => Self::InvalidUsage,
|
||
|
|
ErrorKind::FailedToExecuteCommand(..) => Self::CannotExecute,
|
||
|
|
ErrorKind::FunctionNameShadowsSpecialBuiltin { .. } => Self::InvalidUsage,
|
||
|
|
ErrorKind::IoError(io_err) => io_err.into(),
|
||
|
|
ErrorKind::BuiltinError(inner, ..) => inner.as_exit_code(),
|
||
|
|
_ => Self::GeneralError,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<&std::io::Error> for results::ExecutionExitCode {
|
||
|
|
fn from(io_err: &std::io::Error) -> Self {
|
||
|
|
if io_err.kind() == std::io::ErrorKind::BrokenPipe {
|
||
|
|
Self::BrokenPipe
|
||
|
|
} else {
|
||
|
|
Self::GeneralError
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<&Error> for results::ExecutionExitCode {
|
||
|
|
fn from(error: &Error) -> Self {
|
||
|
|
Self::from(&error.kind)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<T> From<T> for Error
|
||
|
|
where
|
||
|
|
ErrorKind: From<T>,
|
||
|
|
{
|
||
|
|
fn from(convertible_to_kind: T) -> Self {
|
||
|
|
Self {
|
||
|
|
kind: convertible_to_kind.into(),
|
||
|
|
fatal: false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Error {
|
||
|
|
/// Marks this error as fatal.
|
||
|
|
#[must_use]
|
||
|
|
pub const fn into_fatal(mut self) -> Self {
|
||
|
|
self.fatal = true;
|
||
|
|
self
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns whether or not this error is fatal.
|
||
|
|
pub const fn is_fatal(&self) -> bool {
|
||
|
|
self.fatal
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a reference to the error kind.
|
||
|
|
pub const fn kind(&self) -> &ErrorKind {
|
||
|
|
&self.kind
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Try to extract a reference to the underlying `std::io::Error`, if any.
|
||
|
|
pub fn as_io_error(&self) -> Option<&std::io::Error> {
|
||
|
|
match &self.kind {
|
||
|
|
ErrorKind::IoError(io_err) => Some(io_err),
|
||
|
|
ErrorKind::BuiltinError(inner, _) => inner.as_io_error(),
|
||
|
|
_ => None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Converts this error into the appropriate control flow based on the shell's current state.
|
||
|
|
/// This centralizes the logic for determining how fatal errors should affect execution flow.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `shell` - The shell instance, used to check interactive mode and script call stack.
|
||
|
|
pub fn to_control_flow(
|
||
|
|
&self,
|
||
|
|
shell: &Shell<impl extensions::ShellExtensions>,
|
||
|
|
) -> results::ExecutionControlFlow {
|
||
|
|
if self.is_fatal() && !shell.options().interactive {
|
||
|
|
results::ExecutionControlFlow::ExitShell
|
||
|
|
} else {
|
||
|
|
results::ExecutionControlFlow::Normal
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Converts this error into an execution result for the shell.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `shell` - The shell instance, used to determine control flow.
|
||
|
|
pub fn into_result(
|
||
|
|
self,
|
||
|
|
shell: &Shell<impl extensions::ShellExtensions>,
|
||
|
|
) -> results::ExecutionResult {
|
||
|
|
let next_control_flow = self.to_control_flow(shell);
|
||
|
|
let exit_code = results::ExecutionExitCode::from(&self);
|
||
|
|
|
||
|
|
results::ExecutionResult {
|
||
|
|
next_control_flow,
|
||
|
|
exit_code,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Convenience function for returning an error for unimplemented functionality.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `msg` - The message to include in the error
|
||
|
|
pub fn unimp<T>(msg: &'static str) -> Result<T, Error> {
|
||
|
|
Err(ErrorKind::Unimplemented(msg).into())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Convenience function for returning an error for *tracked*, unimplemented functionality.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `msg` - The message to include in the error
|
||
|
|
/// * `project_issue_id` - The GitHub issue ID where the implementation is tracked.
|
||
|
|
pub fn unimp_with_issue<T>(msg: &'static str, project_issue_id: u32) -> Result<T, Error> {
|
||
|
|
Err(ErrorKind::UnimplementedAndTracked(msg, project_issue_id).into())
|
||
|
|
}
|