use std::collections::HashMap;
use super::ast::{ChainOp, Command, Stmt, Word, WordPart};
use super::builtins;
use super::host::{BashHost, Output};
use super::BashError;
pub const DEFAULT_FUEL: u64 = 10_000;
pub const MAX_OUTPUT_BYTES: usize = 256 * 1024;
#[derive(Debug, Clone, Default)]
pub struct ScriptResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
pub struct Evaluator<'h, H: BashHost + ?Sized> {
host: &'h mut H,
vars: HashMap<String, String>,
cwd: String,
last_status: i32,
fuel: u64,
stdout: String,
stderr: String,
}
type Flow = Result<(), BashError>;
impl<'h, H: BashHost + ?Sized> Evaluator<'h, H> {
pub fn new(host: &'h mut H) -> Self {
Self {
host,
vars: HashMap::new(),
cwd: "/".to_string(),
last_status: 0,
fuel: DEFAULT_FUEL,
stdout: String::new(),
stderr: String::new(),
}
}
pub fn with_fuel(mut self, fuel: u64) -> Self {
self.fuel = fuel;
self
}
pub async fn run(mut self, body: &[Stmt]) -> Result<ScriptResult, BashError> {
self.exec_block(body).await?;
Ok(ScriptResult { stdout: self.stdout, stderr: self.stderr, exit_code: self.last_status })
}
fn burn(&mut self) -> Flow {
match self.fuel.checked_sub(1) {
Some(f) => {
self.fuel = f;
Ok(())
}
None => Err(BashError::fuel()),
}
}
fn push_stdout(&mut self, s: &str) -> Flow {
if self.stdout.len() + s.len() > MAX_OUTPUT_BYTES {
return Err(BashError::other(format!(
"output exceeded {} bytes",
MAX_OUTPUT_BYTES
)));
}
self.stdout.push_str(s);
Ok(())
}
async fn exec_block(&mut self, body: &[Stmt]) -> Flow {
for stmt in body {
self.exec_stmt(stmt).await?;
}
Ok(())
}
fn exec_stmt<'s>(&'s mut self, stmt: &'s Stmt) -> super::BoxFut<'s, Flow> {
Box::pin(async move {
self.burn()?;
match stmt {
Stmt::Assign { name, value } => {
let v = self.expand_word(value).await?;
self.vars.insert(name.clone(), v);
self.last_status = 0;
}
Stmt::Pipeline(cmds) => {
let out = self.run_pipeline(cmds).await?;
self.push_stdout(&out.stdout)?;
if !out.stderr.is_empty() {
self.stderr.push_str(&out.stderr);
}
self.last_status = out.code;
}
Stmt::AndOr { pipelines, ops } => {
let first = self.run_pipeline(&pipelines[0]).await?;
self.push_stdout(&first.stdout)?;
if !first.stderr.is_empty() {
self.stderr.push_str(&first.stderr);
}
let mut code = first.code;
for (op, pipe) in ops.iter().zip(&pipelines[1..]) {
let run_next = match op {
ChainOp::And => code == 0,
ChainOp::Or => code != 0,
};
if !run_next {
continue;
}
self.burn()?;
let out = self.run_pipeline(pipe).await?;
self.push_stdout(&out.stdout)?;
if !out.stderr.is_empty() {
self.stderr.push_str(&out.stderr);
}
code = out.code;
}
self.last_status = code;
}
Stmt::If { arms, otherwise } => {
let mut ran = false;
for (cond, body) in arms {
if self.eval_condition(cond).await? {
self.exec_block(body).await?;
ran = true;
break;
}
}
if !ran {
if let Some(body) = otherwise {
self.exec_block(body).await?;
} else {
self.last_status = 0;
}
}
}
Stmt::For { var, items, body } => {
let mut values = Vec::new();
for w in items {
let expanded = self.expand_word(w).await?;
values.extend(expanded.split_whitespace().map(String::from));
}
for v in values {
self.burn()?;
self.vars.insert(var.clone(), v);
self.exec_block(body).await?;
}
}
Stmt::While { cond, body } => {
while self.eval_condition(cond).await? {
self.burn()?;
self.exec_block(body).await?;
}
}
}
Ok(())
})
}
async fn eval_condition(&mut self, cond: &[Stmt]) -> Result<bool, BashError> {
self.exec_block(cond).await?;
Ok(self.last_status == 0)
}
async fn run_pipeline(&mut self, cmds: &[Command]) -> Result<Output, BashError> {
let mut stdin = String::new();
let mut last = Output::default();
let mut accumulated_stderr = String::new();
for (i, cmd) in cmds.iter().enumerate() {
self.burn()?;
let out = self.run_command(cmd, &stdin).await?;
accumulated_stderr.push_str(&out.stderr);
if i + 1 < cmds.len() {
stdin = out.stdout;
} else {
last = out;
}
}
last.stderr = accumulated_stderr;
Ok(last)
}
async fn run_command(&mut self, cmd: &Command, stdin: &str) -> Result<Output, BashError> {
let name = self.expand_word(&cmd.name).await?;
let mut args = Vec::with_capacity(cmd.args.len());
for a in &cmd.args {
args.push(self.expand_word(a).await?);
}
if name == "cd" {
let target = args.first().map(String::as_str).unwrap_or("/");
let next = builtins::resolve(&self.cwd, target);
match self.host.fs().metadata(&next).await {
Ok(Some(m)) if m.kind == crate::filesystem::EntryKind::Directory => {
self.cwd = next;
return Ok(Output::ok(""));
}
Ok(Some(_)) => return Ok(Output::err(format!("cd: {target}: not a directory"), 1)),
Ok(None) => {
if next == "/" {
self.cwd = next;
return Ok(Output::ok(""));
}
return Ok(Output::err(format!("cd: {target}: no such file or directory"), 1));
}
Err(e) => return Ok(Output::err(format!("cd: {target}: {e}"), 1)),
}
}
if name == "pwd" {
return Ok(Output::ok(format!("{}\n", self.cwd)));
}
if name == "run" || name == "." || name == "source" {
let Some(path_arg) = args.first() else {
return Ok(Output::err(format!("{name}: missing script path"), 2));
};
let path = builtins::resolve(&self.cwd, path_arg);
let src = match self.host.fs().read(&path).await {
Ok(b) => String::from_utf8_lossy(&b).into_owned(),
Err(e) => return Ok(Output::err(format!("{name}: {path_arg}: {e}"), 1)),
};
return self.run_nested(&src, path_arg).await;
}
let cwd = self.cwd.clone();
Ok(self.host.run_builtin(&cwd, &name, &args, stdin).await)
}
async fn run_nested(&mut self, src: &str, label: &str) -> Result<Output, BashError> {
let body = match super::lexer::lex(src).and_then(|t| super::parser::parse(&t)) {
Ok(b) => b,
Err(e) => return Ok(Output::err(format!("run: {label}: {e}"), 2)),
};
let mut sub = Evaluator {
host: self.host,
vars: HashMap::new(),
cwd: self.cwd.clone(),
last_status: 0,
fuel: self.fuel,
stdout: String::new(),
stderr: String::new(),
};
let flow = sub.exec_block(&body).await;
self.fuel = sub.fuel;
flow?;
Ok(Output { stdout: sub.stdout, stderr: sub.stderr, code: sub.last_status })
}
fn expand_word<'s>(&'s mut self, word: &'s Word) -> super::BoxFut<'s, Result<String, BashError>> {
Box::pin(async move {
let mut out = String::new();
for part in word {
match part {
WordPart::Lit(s) => out.push_str(s),
WordPart::Var(name) => {
if name == "?" {
out.push_str(&self.last_status.to_string());
} else {
out.push_str(self.vars.get(name).map(String::as_str).unwrap_or(""));
}
}
WordPart::Subst(src) => {
let captured = self.run_substitution(src).await?;
out.push_str(&captured);
}
}
}
Ok(out)
})
}
async fn run_substitution(&mut self, src: &str) -> Result<String, BashError> {
let tokens = super::lexer::lex(src)?;
let body = super::parser::parse(&tokens)?;
let mut sub = Evaluator {
host: self.host,
vars: self.vars.clone(),
cwd: self.cwd.clone(),
last_status: self.last_status,
fuel: self.fuel,
stdout: String::new(),
stderr: String::new(),
};
sub.exec_block(&body).await?;
self.fuel = sub.fuel;
self.last_status = sub.last_status;
self.stderr.push_str(&sub.stderr);
let mut captured = sub.stdout;
while captured.ends_with('\n') {
captured.pop();
}
Ok(captured)
}
}