yosh 0.1.0

A POSIX-compliant shell implemented in Rust
Documentation
use std::fmt;

#[derive(Debug, Clone, PartialEq)]
pub struct ShellError {
    pub kind: ShellErrorKind,
    pub message: String,
    pub location: Option<SourceLocation>,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SourceLocation {
    pub line: usize,
    pub column: usize,
}

#[derive(Debug, Clone, PartialEq)]
pub enum ShellErrorKind {
    Parse(ParseErrorKind),
    Expansion(ExpansionErrorKind),
    #[allow(dead_code)] // planned: runtime error migration (see TODO.md)
    Runtime(RuntimeErrorKind),
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ParseErrorKind {
    UnterminatedSingleQuote,
    UnterminatedDoubleQuote,
    UnterminatedCommandSub,
    UnterminatedArithSub,
    UnterminatedParamExpansion,
    UnterminatedBacktick,
    UnterminatedDollarSingleQuote,
    UnexpectedToken,
    #[allow(dead_code)] // matched in parse_status; will be constructed by parser enhancements
    UnexpectedEof,
    InvalidRedirect,
    #[allow(dead_code)] // will be constructed by function definition parsing
    InvalidFunctionName,
    InvalidHereDoc,
}

#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(dead_code)]
pub enum ExpansionErrorKind {
    DivisionByZero,
    UnsetVariable,
    ParameterError,
    InvalidArithmetic,
}

#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(dead_code)]
pub enum RuntimeErrorKind {
    CommandNotFound,
    PermissionDenied,
    RedirectFailed,
    ReadonlyVariable,
    InvalidOption,
}

impl fmt::Display for ShellError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.location {
            Some(loc) => write!(f, "yosh: line {}: {}", loc.line, self.message),
            None => write!(f, "yosh: {}", self.message),
        }
    }
}

impl std::error::Error for ShellError {}

impl ShellError {
    pub fn parse(kind: ParseErrorKind, line: usize, column: usize, message: impl Into<String>) -> Self {
        Self {
            kind: ShellErrorKind::Parse(kind),
            message: message.into(),
            location: Some(SourceLocation { line, column }),
        }
    }

    pub fn expansion(kind: ExpansionErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind: ShellErrorKind::Expansion(kind),
            message: message.into(),
            location: None,
        }
    }

    #[allow(dead_code)] // planned: runtime error migration (see TODO.md)
    pub fn runtime(kind: RuntimeErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind: ShellErrorKind::Runtime(kind),
            message: message.into(),
            location: None,
        }
    }
}

pub type Result<T> = std::result::Result<T, ShellError>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_display_with_location() {
        let err = ShellError::parse(
            ParseErrorKind::UnexpectedToken,
            5,
            10,
            "unexpected ')'",
        );
        assert_eq!(err.to_string(), "yosh: line 5: unexpected ')'");
    }

    #[test]
    fn test_error_display_without_location() {
        let err = ShellError::runtime(
            RuntimeErrorKind::CommandNotFound,
            "foo: not found",
        );
        assert_eq!(err.to_string(), "yosh: foo: not found");
    }
}