mod lex;
mod parse;
use std::error::Error;
use std::fmt;
pub use parse::parse;
pub type ParseResult<T> = Result<T, ParseError>;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Program {
pub lists: Vec<AndOrList>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AndOrList {
pub first: Pipeline,
pub rest: Vec<AndOr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AndOr {
pub op: AndOrOp,
pub pipeline: Pipeline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AndOrOp {
And,
Or,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pipeline {
pub commands: Vec<Command>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
Simple(SimpleCommand),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SimpleCommand {
pub assignments: Vec<Assignment>,
pub words: Vec<Word>,
pub redirects: Vec<Redirect>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Assignment {
pub name: String,
pub value: Word,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Word {
pub segments: Vec<Segment>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Segment {
Literal {
value: String,
quoted: bool,
},
Expansion {
name: String,
quoted: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Redirect {
pub fd: Option<u32>,
pub op: RedirectOp,
pub target: RedirectTarget,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RedirectOp {
Read,
Write,
Append,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RedirectTarget {
Word(Word),
Fd(u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub position: usize,
pub kind: ParseErrorKind,
}
impl ParseError {
pub(crate) const fn new(position: usize, kind: ParseErrorKind) -> Self {
Self { position, kind }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseErrorKind {
UnexpectedEof {
expected: &'static str,
},
UnexpectedToken {
expected: &'static str,
found: &'static str,
},
MissingRedirectTarget,
UnterminatedQuote {
quote: QuoteKind,
},
UnterminatedEscape,
UnterminatedBracedExpansion,
InvalidParameterExpansion,
InvalidFileDescriptor,
AmbiguousRedirect,
Unsupported(UnsupportedConstruct),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuoteKind {
Single,
Double,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnsupportedConstruct {
Glob,
CommandSubstitution,
Backticks,
Background,
Subshell,
BraceExpansion,
Heredoc,
TildeExpansion,
ParameterExpansion,
RedirectFdDup,
AnsiCString,
LocaleTranslation,
AppendAssignment,
PipelineNegation,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "parse error at byte {}: ", self.position)?;
match &self.kind {
ParseErrorKind::UnexpectedEof { expected } => {
write!(f, "expected {expected}, found end of input")
}
ParseErrorKind::UnexpectedToken { expected, found } => {
write!(f, "expected {expected}, found {found}")
}
ParseErrorKind::MissingRedirectTarget => write!(f, "missing redirect target"),
ParseErrorKind::UnterminatedQuote { quote } => match quote {
QuoteKind::Single => write!(f, "unterminated single quote"),
QuoteKind::Double => write!(f, "unterminated double quote"),
},
ParseErrorKind::UnterminatedEscape => write!(f, "unterminated escape"),
ParseErrorKind::UnterminatedBracedExpansion => {
write!(f, "unterminated braced parameter expansion")
}
ParseErrorKind::InvalidParameterExpansion => {
write!(f, "invalid or unsupported parameter expansion")
}
ParseErrorKind::InvalidFileDescriptor => write!(f, "invalid file descriptor"),
ParseErrorKind::AmbiguousRedirect => write!(f, "ambiguous redirect"),
ParseErrorKind::Unsupported(construct) => write!(f, "unsupported {construct}"),
}
}
}
impl Error for ParseError {}
impl fmt::Display for UnsupportedConstruct {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::Glob => "glob pattern",
Self::CommandSubstitution => "command substitution",
Self::Backticks => "backtick command substitution",
Self::Background => "background execution",
Self::Subshell => "subshell syntax",
Self::BraceExpansion => "brace expansion",
Self::Heredoc => "heredoc",
Self::TildeExpansion => "tilde expansion",
Self::ParameterExpansion => "parameter expansion",
Self::RedirectFdDup => "redirect fd duplication",
Self::AnsiCString => "ANSI-C quoting",
Self::LocaleTranslation => "locale translation quoting",
Self::AppendAssignment => "append assignment",
Self::PipelineNegation => "pipeline negation",
};
f.write_str(name)
}
}
#[cfg(test)]
mod tests {
use super::{ParseError, ParseErrorKind, QuoteKind, UnsupportedConstruct as Unsupported};
#[test]
fn parse_error_display_messages_are_exact() {
let cases = [
(
ParseErrorKind::UnexpectedEof {
expected: "command",
},
"parse error at byte 7: expected command, found end of input",
),
(
ParseErrorKind::UnexpectedToken {
expected: "word",
found: "pipe",
},
"parse error at byte 7: expected word, found pipe",
),
(
ParseErrorKind::MissingRedirectTarget,
"parse error at byte 7: missing redirect target",
),
(
ParseErrorKind::UnterminatedQuote {
quote: QuoteKind::Single,
},
"parse error at byte 7: unterminated single quote",
),
(
ParseErrorKind::UnterminatedQuote {
quote: QuoteKind::Double,
},
"parse error at byte 7: unterminated double quote",
),
(
ParseErrorKind::UnterminatedEscape,
"parse error at byte 7: unterminated escape",
),
(
ParseErrorKind::UnterminatedBracedExpansion,
"parse error at byte 7: unterminated braced parameter expansion",
),
(
ParseErrorKind::InvalidParameterExpansion,
"parse error at byte 7: invalid or unsupported parameter expansion",
),
(
ParseErrorKind::InvalidFileDescriptor,
"parse error at byte 7: invalid file descriptor",
),
(
ParseErrorKind::AmbiguousRedirect,
"parse error at byte 7: ambiguous redirect",
),
];
for (kind, expected) in cases {
assert_eq!(ParseError::new(7, kind).to_string(), expected);
}
}
#[test]
fn unsupported_construct_display_messages_are_exact() {
let cases = [
(Unsupported::Glob, "glob pattern"),
(Unsupported::CommandSubstitution, "command substitution"),
(Unsupported::Backticks, "backtick command substitution"),
(Unsupported::Background, "background execution"),
(Unsupported::Subshell, "subshell syntax"),
(Unsupported::BraceExpansion, "brace expansion"),
(Unsupported::Heredoc, "heredoc"),
(Unsupported::TildeExpansion, "tilde expansion"),
(Unsupported::ParameterExpansion, "parameter expansion"),
(Unsupported::RedirectFdDup, "redirect fd duplication"),
(Unsupported::AnsiCString, "ANSI-C quoting"),
(Unsupported::LocaleTranslation, "locale translation quoting"),
(Unsupported::AppendAssignment, "append assignment"),
(Unsupported::PipelineNegation, "pipeline negation"),
];
for (construct, expected) in cases {
assert_eq!(construct.to_string(), expected);
assert_eq!(
ParseError::new(3, ParseErrorKind::Unsupported(construct)).to_string(),
format!("parse error at byte 3: unsupported {expected}")
);
}
}
}