use crate::parser::ast::{Command, Op, Pipeline, Program, Redirection};
use crate::parser::chain::parse_builder_chain;
use crate::parser::errors::ParseError;
use crate::parser::lexer::{lex, Token};
use crate::parser::normalize::normalize_name;
use crate::parser::normalize::parse_test_id;
use crate::parser::priority::infer_priority;
use crate::parser::suffixes::{strip_optional, strip_urgency, strip_verbosity};
#[must_use = "parsing produces a Program AST; ignoring it is likely a bug"]
pub fn parse(input: &str) -> Result<Program, ParseError> {
let tokens = lex(input)?;
let mut pipelines: Vec<Pipeline> = Vec::new();
let mut commands: Vec<Command> = Vec::new();
let mut after_redirection = false;
let mut i = 0;
while i < tokens.len() {
let token = &tokens[i];
if after_redirection && !matches!(token, Token::And | Token::Or) {
return Err(ParseError::InvalidRedirection {
token: token_str(token),
position: i,
});
}
match token {
Token::And | Token::Or => {
if commands.is_empty() {
return Err(ParseError::MissingOperator {
token: token_str(token),
position: i,
});
}
let op = match token {
Token::And => Op::And,
Token::Or => Op::Or,
_ => unreachable!(),
};
pipelines.push(Pipeline {
commands,
operator: Some(op),
});
commands = Vec::new();
after_redirection = false;
i += 1;
}
Token::Pipe | Token::PipeErr => {
if commands.is_empty() {
return Err(ParseError::MissingOperator {
token: token_str(token),
position: i,
});
}
let op = match token {
Token::Pipe => Op::Pipe,
Token::PipeErr => Op::PipeErr,
_ => unreachable!(),
};
if let Some(last) = commands.last_mut() {
if last.pipe.is_some() {
return Err(ParseError::MissingOperator {
token: token_str(token),
position: i,
});
}
last.pipe = Some(op);
}
i += 1;
}
Token::Redirect | Token::Append => {
if commands.is_empty() {
return Err(ParseError::InvalidRedirection {
token: token_str(token),
position: i,
});
}
if i + 1 >= tokens.len() {
return Err(ParseError::InvalidRedirection {
token: token_str(token),
position: i,
});
}
let target_token = &tokens[i + 1];
if matches!(
target_token,
Token::And
| Token::Or
| Token::Pipe
| Token::PipeErr
| Token::Redirect
| Token::Append
) {
return Err(ParseError::InvalidRedirection {
token: token_str(token),
position: i,
});
}
let target = match target_token {
Token::Word(w) => w.clone(),
Token::Command(c) => c.clone(),
_ => {
return Err(ParseError::InvalidRedirection {
token: token_str(token),
position: i,
})
}
};
let redirect = if matches!(token, Token::Redirect) {
Redirection::Truncate(target)
} else {
Redirection::Append(target)
};
if let Some(last) = commands.last_mut() {
last.redirect = Some(redirect);
}
after_redirection = true;
i += 2;
}
Token::Word(w) => {
return Err(if w.starts_with('!') {
ParseError::InvalidBang {
token: w.clone(),
position: i,
}
} else {
ParseError::MissingOperator {
token: w.clone(),
position: i,
}
});
}
Token::Command(raw) => {
if let Some(last) = commands.last() {
if last.pipe.is_none() {
return Err(ParseError::MissingOperator {
token: raw.clone(),
position: i,
});
}
}
let (after_urgency, urgency) =
strip_urgency(raw).ok_or_else(|| ParseError::InvalidBang {
token: raw.clone(),
position: i,
})?;
let (after_verbosity, verbosity) = strip_verbosity(after_urgency);
let (bare_token, optional) =
strip_optional(after_verbosity).map_err(|_| ParseError::InvalidSuffix {
token: raw.clone(),
position: i,
})?;
let parts =
parse_builder_chain(bare_token).map_err(|_| ParseError::MalformedChain {
token: raw.clone(),
position: i,
})?;
let (cmd_raw, args) = (parts.name, parts.args);
if cmd_raw.chars().any(|c| c.is_ascii_digit()) && parse_test_id(&cmd_raw).is_none()
{
return Err(ParseError::InvalidDigits {
token: raw.clone(),
position: i,
});
}
let name = normalize_name(&cmd_raw);
let test_id = parse_test_id(&cmd_raw);
let mut cmd = Command::new(name, infer_priority(&cmd_raw));
cmd.raw = raw.to_string();
cmd.urgency = urgency;
cmd.verbosity = verbosity;
cmd.optional = optional;
cmd.primary = parts.primary;
cmd.args = args;
cmd.test_id = test_id;
commands.push(cmd);
after_redirection = false;
i += 1;
}
}
}
if !commands.is_empty() {
pipelines.push(Pipeline {
commands,
operator: None,
});
}
Ok(Program { pipelines })
}
pub mod ast;
pub mod chain;
pub mod errors;
pub mod lexer;
pub mod normalize;
pub mod priority;
pub mod suffixes;
fn token_str(token: &Token) -> String {
match token {
Token::Command(s) | Token::Word(s) => s.clone(),
Token::Pipe => "|".to_string(),
Token::PipeErr => "|&".to_string(),
Token::And => "&&".to_string(),
Token::Or => "||".to_string(),
Token::Redirect => ">".to_string(),
Token::Append => ">>".to_string(),
}
}