use super::ast::{Command, Stmt, Word};
use super::token::{Token, WordPart};
use super::BashError;
pub fn parse(tokens: &[Token]) -> Result<Vec<Stmt>, BashError> {
let mut p = Parser { toks: tokens, pos: 0 };
let body = p.block(&[])?;
p.expect_eof()?;
Ok(body)
}
struct Parser<'a> {
toks: &'a [Token],
pos: usize,
}
impl<'a> Parser<'a> {
fn peek(&self) -> &Token {
self.toks.get(self.pos).unwrap_or(&Token::Eof)
}
fn bump(&mut self) -> &'a Token {
let t = self.toks.get(self.pos).unwrap_or(&Token::Eof);
if self.pos < self.toks.len() {
self.pos += 1;
}
t
}
fn expect_eof(&self) -> Result<(), BashError> {
match self.peek() {
Token::Eof => Ok(()),
other => Err(BashError::parse(format!("unexpected trailing token: {other:?}"))),
}
}
fn skip_semis(&mut self) {
while matches!(self.peek(), Token::Semi) {
self.pos += 1;
}
}
fn at_keyword(&self, kw: &str) -> bool {
matches!(self.peek(), Token::Word(parts) if is_kw(parts, kw))
}
fn eat_keyword(&mut self, kw: &str) -> Result<(), BashError> {
if self.at_keyword(kw) {
self.pos += 1;
Ok(())
} else {
Err(BashError::parse(format!("expected `{kw}`, found {:?}", self.peek())))
}
}
fn block(&mut self, terminators: &[&str]) -> Result<Vec<Stmt>, BashError> {
let mut stmts = Vec::new();
loop {
self.skip_semis();
if matches!(self.peek(), Token::Eof) {
break;
}
if terminators.iter().any(|kw| self.at_keyword(kw)) {
break;
}
stmts.push(self.statement()?);
match self.peek() {
Token::Semi | Token::Eof => {}
_ if terminators.iter().any(|kw| self.at_keyword(kw)) => {}
other => {
return Err(BashError::parse(format!(
"expected `;` or newline between commands, found {other:?}"
)))
}
}
}
Ok(stmts)
}
fn statement(&mut self) -> Result<Stmt, BashError> {
if self.at_keyword("if") {
return self.if_stmt();
}
if self.at_keyword("for") {
return self.for_stmt();
}
if self.at_keyword("while") {
return self.while_stmt();
}
if let Token::Word(parts) = self.peek() {
if let Some((name, value)) = split_assignment(parts) {
self.pos += 1;
if let Token::Word(_) = self.peek() {
return Err(BashError::parse(
"env-prefix assignment (`X=v cmd`) is not supported; put the assignment on its own line",
));
}
return Ok(Stmt::Assign { name, value });
}
}
self.pipeline()
}
fn pipeline(&mut self) -> Result<Stmt, BashError> {
let mut cmds = vec![self.command()?];
while matches!(self.peek(), Token::Pipe) {
self.pos += 1;
cmds.push(self.command()?);
}
Ok(Stmt::Pipeline(cmds))
}
fn command(&mut self) -> Result<Command, BashError> {
let name = match self.bump() {
Token::Word(parts) => parts.clone(),
other => return Err(BashError::parse(format!("expected a command, found {other:?}"))),
};
let mut args = Vec::new();
while let Token::Word(parts) = self.peek() {
args.push(parts.clone());
self.pos += 1;
}
Ok(Command { name, args })
}
fn if_stmt(&mut self) -> Result<Stmt, BashError> {
self.eat_keyword("if")?;
let mut arms = Vec::new();
let cond = self.block(&["then"])?;
self.eat_keyword("then")?;
let body = self.block(&["elif", "else", "fi"])?;
arms.push((cond, body));
while self.at_keyword("elif") {
self.eat_keyword("elif")?;
let cond = self.block(&["then"])?;
self.eat_keyword("then")?;
let body = self.block(&["elif", "else", "fi"])?;
arms.push((cond, body));
}
let otherwise = if self.at_keyword("else") {
self.eat_keyword("else")?;
Some(self.block(&["fi"])?)
} else {
None
};
self.eat_keyword("fi")?;
Ok(Stmt::If { arms, otherwise })
}
fn for_stmt(&mut self) -> Result<Stmt, BashError> {
self.eat_keyword("for")?;
let var = match self.bump() {
Token::Word(parts) => single_ident(parts)
.ok_or_else(|| BashError::parse("for: expected a variable name"))?,
other => return Err(BashError::parse(format!("for: expected a name, found {other:?}"))),
};
self.eat_keyword("in")?;
let mut items = Vec::new();
loop {
match self.peek() {
Token::Word(parts) if !is_kw(parts, "do") => {
items.push(parts.clone());
self.pos += 1;
}
_ => break,
}
}
self.skip_semis();
self.eat_keyword("do")?;
let body = self.block(&["done"])?;
self.eat_keyword("done")?;
Ok(Stmt::For { var, items, body })
}
fn while_stmt(&mut self) -> Result<Stmt, BashError> {
self.eat_keyword("while")?;
let cond = self.block(&["do"])?;
self.eat_keyword("do")?;
let body = self.block(&["done"])?;
self.eat_keyword("done")?;
Ok(Stmt::While { cond, body })
}
}
fn is_kw(parts: &[WordPart], kw: &str) -> bool {
matches!(parts, [WordPart::Lit(s)] if s == kw)
}
fn split_assignment(parts: &[WordPart]) -> Option<(String, Word)> {
let WordPart::Lit(first) = parts.first()? else {
return None;
};
let eq = first.find('=')?;
let name = &first[..eq];
if name.is_empty() || !is_ident(name) {
return None;
}
let mut value: Word = Vec::new();
let rest = &first[eq + 1..];
if !rest.is_empty() {
value.push(WordPart::Lit(rest.to_string()));
}
value.extend_from_slice(&parts[1..]);
Some((name.to_string(), value))
}
fn single_ident(parts: &[WordPart]) -> Option<String> {
match parts {
[WordPart::Lit(s)] if is_ident(s) => Some(s.clone()),
_ => None,
}
}
pub(crate) fn is_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
}