use std::fmt;
use crate::arithmetic;
use crate::ast::{BinaryOp, Expr, FileTestOp, Pipeline, StringPart, StringTestOp, TestCmpOp, TestExpr, Value, VarPath};
use crate::vfs::DirEntry;
use std::path::Path;
use super::result::ExecResult;
use super::scope::Scope;
pub fn strip_leading_tabs(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut at_line_start = true;
for ch in s.chars() {
if at_line_start && ch == '\t' {
continue;
}
out.push(ch);
at_line_start = ch == '\n';
}
out
}
#[derive(Debug, Clone, PartialEq)]
pub enum EvalError {
UndefinedVariable(String),
InvalidPath(String),
TypeError { expected: &'static str, got: String },
CommandFailed(String),
NoExecutor,
ArithmeticError(String),
RegexError(String),
}
impl fmt::Display for EvalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EvalError::UndefinedVariable(name) => write!(f, "undefined variable: {name}"),
EvalError::InvalidPath(path) => write!(f, "invalid path: {path}"),
EvalError::TypeError { expected, got } => {
write!(f, "type error: expected {expected}, got {got}")
}
EvalError::CommandFailed(msg) => write!(f, "command failed: {msg}"),
EvalError::NoExecutor => write!(f, "no executor available for command substitution"),
EvalError::ArithmeticError(msg) => write!(f, "arithmetic error: {msg}"),
EvalError::RegexError(msg) => write!(f, "regex error: {msg}"),
}
}
}
impl std::error::Error for EvalError {}
pub type EvalResult<T> = Result<T, EvalError>;
pub trait Executor {
fn execute(&mut self, pipeline: &Pipeline, scope: &mut Scope) -> EvalResult<ExecResult>;
fn file_stat(&self, path: &Path) -> Option<DirEntry> {
std::fs::metadata(path).ok().map(|meta| {
if meta.is_dir() {
DirEntry::directory(path.file_name().unwrap_or_default().to_string_lossy())
} else {
#[allow(unused_mut)]
let mut entry = DirEntry::file(
path.file_name().unwrap_or_default().to_string_lossy(),
meta.len(),
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
entry.permissions = Some(meta.permissions().mode());
}
entry
}
})
}
}
pub struct NoOpExecutor;
impl Executor for NoOpExecutor {
fn execute(&mut self, _pipeline: &Pipeline, _scope: &mut Scope) -> EvalResult<ExecResult> {
Err(EvalError::NoExecutor)
}
}
pub struct Evaluator<'a, E: Executor> {
scope: &'a mut Scope,
executor: &'a mut E,
}
impl<'a, E: Executor> Evaluator<'a, E> {
pub fn new(scope: &'a mut Scope, executor: &'a mut E) -> Self {
Self { scope, executor }
}
pub fn eval(&mut self, expr: &Expr) -> EvalResult<Value> {
match expr {
Expr::Literal(value) => self.eval_literal(value),
Expr::VarRef(path) => self.eval_var_ref(path),
Expr::Interpolated(parts) => self.eval_interpolated(parts),
Expr::HereDocBody { parts, strip_tabs } => {
let unwrapped: Vec<StringPart> =
parts.iter().map(|sp| sp.part.clone()).collect();
let value = self.eval_interpolated(&unwrapped)?;
if *strip_tabs {
if let Value::String(s) = value {
Ok(Value::String(strip_leading_tabs(&s)))
} else {
Ok(value)
}
} else {
Ok(value)
}
}
Expr::BinaryOp { left, op, right } => self.eval_binary_op(left, *op, right),
Expr::CommandSubst(pipeline) => self.eval_command_subst(pipeline),
Expr::Test(test_expr) => self.eval_test(test_expr),
Expr::Positional(n) => self.eval_positional(*n),
Expr::AllArgs => self.eval_all_args(),
Expr::ArgCount => self.eval_arg_count(),
Expr::VarLength(name) => self.eval_var_length(name),
Expr::VarWithDefault { name, default } => self.eval_var_with_default(name, default),
Expr::Arithmetic(expr_str) => self.eval_arithmetic(expr_str),
Expr::Command(cmd) => self.eval_command(cmd),
Expr::LastExitCode => self.eval_last_exit_code(),
Expr::CurrentPid => self.eval_current_pid(),
Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
}
}
fn eval_last_exit_code(&self) -> EvalResult<Value> {
Ok(Value::Int(self.scope.last_result().code))
}
fn eval_current_pid(&self) -> EvalResult<Value> {
Ok(Value::Int(self.scope.pid() as i64))
}
fn eval_command(&mut self, cmd: &crate::ast::Command) -> EvalResult<Value> {
match cmd.name.as_str() {
"true" => return Ok(Value::Bool(true)),
"false" => return Ok(Value::Bool(false)),
_ => {}
}
let pipeline = crate::ast::Pipeline {
commands: vec![cmd.clone()],
background: false,
};
let result = self.executor.execute(&pipeline, self.scope)?;
Ok(Value::Bool(result.code == 0))
}
fn eval_arithmetic(&mut self, expr_str: &str) -> EvalResult<Value> {
arithmetic::eval_arithmetic(expr_str, self.scope)
.map(Value::Int)
.map_err(|e| EvalError::ArithmeticError(e.to_string()))
}
fn eval_test(&mut self, test_expr: &TestExpr) -> EvalResult<Value> {
let result = match test_expr {
TestExpr::FileTest { op, path } => {
let path_value = self.eval(path)?;
let path_str = value_to_string(&path_value);
let path = Path::new(&path_str);
let entry = self.executor.file_stat(path);
match op {
FileTestOp::Exists => entry.is_some(),
FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
FileTestOp::Readable => entry.is_some(),
FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
e.permissions.is_none_or(|p| p & 0o222 != 0)
}),
FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
e.permissions.is_some_and(|p| p & 0o111 != 0)
}),
}
}
TestExpr::StringTest { op, value } => {
let val = self.eval(value)?;
let s = value_to_string(&val);
match op {
StringTestOp::IsEmpty => s.is_empty(),
StringTestOp::IsNonEmpty => !s.is_empty(),
}
}
TestExpr::Comparison { left, op, right } => {
let left_val = self.eval(left)?;
let right_val = self.eval(right)?;
match op {
TestCmpOp::Eq => values_equal(&left_val, &right_val),
TestCmpOp::NotEq => !values_equal(&left_val, &right_val),
TestCmpOp::Match => {
match regex_match(&left_val, &right_val, false) {
Ok(Value::Bool(b)) => b,
Ok(_) => false,
Err(_) => false,
}
}
TestCmpOp::NotMatch => {
match regex_match(&left_val, &right_val, true) {
Ok(Value::Bool(b)) => b,
Ok(_) => true,
Err(_) => true,
}
}
TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
let ord = compare_values(&left_val, &right_val)?;
match op {
TestCmpOp::Gt => ord.is_gt(),
TestCmpOp::Lt => ord.is_lt(),
TestCmpOp::GtEq => ord.is_ge(),
TestCmpOp::LtEq => ord.is_le(),
_ => unreachable!(),
}
}
TestCmpOp::NumEq
| TestCmpOp::NumNotEq
| TestCmpOp::NumGt
| TestCmpOp::NumLt
| TestCmpOp::NumGtEq
| TestCmpOp::NumLtEq => {
let ord = numeric_compare(&left_val, &right_val)?;
match op {
TestCmpOp::NumEq => ord.is_eq(),
TestCmpOp::NumNotEq => !ord.is_eq(),
TestCmpOp::NumGt => ord.is_gt(),
TestCmpOp::NumLt => ord.is_lt(),
TestCmpOp::NumGtEq => ord.is_ge(),
TestCmpOp::NumLtEq => ord.is_le(),
_ => unreachable!(),
}
}
}
}
TestExpr::And { left, right } => {
let left_result = self.eval_test(left)?;
if !value_to_bool(&left_result) {
false } else {
value_to_bool(&self.eval_test(right)?)
}
}
TestExpr::Or { left, right } => {
let left_result = self.eval_test(left)?;
if value_to_bool(&left_result) {
true } else {
value_to_bool(&self.eval_test(right)?)
}
}
TestExpr::Not { expr } => {
let result = self.eval_test(expr)?;
!value_to_bool(&result)
}
};
Ok(Value::Bool(result))
}
fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
Ok(value.clone())
}
fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
self.scope
.resolve_path(path)
.ok_or_else(|| EvalError::InvalidPath(format_path(path)))
}
fn eval_positional(&self, n: usize) -> EvalResult<Value> {
match self.scope.get_positional(n) {
Some(s) => Ok(Value::String(s.to_string())),
None => Ok(Value::String(String::new())), }
}
fn eval_all_args(&self) -> EvalResult<Value> {
let args = self.scope.all_args();
Ok(Value::String(args.join(" ")))
}
fn eval_arg_count(&self) -> EvalResult<Value> {
Ok(Value::Int(self.scope.arg_count() as i64))
}
fn eval_var_length(&self, name: &str) -> EvalResult<Value> {
match self.scope.get(name) {
Some(value) => {
let s = value_to_string(value);
Ok(Value::Int(s.len() as i64))
}
None => Ok(Value::Int(0)), }
}
fn eval_var_with_default(&mut self, name: &str, default: &[StringPart]) -> EvalResult<Value> {
match self.scope.get(name) {
Some(value) => {
let s = value_to_string(value);
if s.is_empty() {
self.eval_interpolated(default)
} else {
Ok(value.clone())
}
}
None => {
self.eval_interpolated(default)
}
}
}
fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
let mut result = String::new();
for part in parts {
match part {
StringPart::Literal(s) => result.push_str(s),
StringPart::Var(path) => {
if let Some(value) = self.scope.resolve_path(path) {
result.push_str(&value_to_string(&value));
}
}
StringPart::VarWithDefault { name, default } => {
let value = self.eval_var_with_default(name, default)?;
result.push_str(&value_to_string(&value));
}
StringPart::VarLength(name) => {
let value = self.eval_var_length(name)?;
result.push_str(&value_to_string(&value));
}
StringPart::Positional(n) => {
let value = self.eval_positional(*n)?;
result.push_str(&value_to_string(&value));
}
StringPart::AllArgs => {
let value = self.eval_all_args()?;
result.push_str(&value_to_string(&value));
}
StringPart::ArgCount => {
let value = self.eval_arg_count()?;
result.push_str(&value_to_string(&value));
}
StringPart::Arithmetic(expr) => {
let value = self.eval_arithmetic_string(expr)?;
result.push_str(&value_to_string(&value));
}
StringPart::CommandSubst(pipeline) => {
let value = self.eval_command_subst(pipeline)?;
result.push_str(&value_to_string(&value));
}
StringPart::LastExitCode => {
result.push_str(&self.scope.last_result().code.to_string());
}
StringPart::CurrentPid => {
result.push_str(&self.scope.pid().to_string());
}
}
}
Ok(Value::String(result))
}
fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
arithmetic::eval_arithmetic(expr, self.scope)
.map(Value::Int)
.map_err(|e| EvalError::ArithmeticError(e.to_string()))
}
fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
match op {
BinaryOp::And => {
let left_val = self.eval(left)?;
if !is_truthy(&left_val) {
return Ok(left_val);
}
self.eval(right)
}
BinaryOp::Or => {
let left_val = self.eval(left)?;
if is_truthy(&left_val) {
return Ok(left_val);
}
self.eval(right)
}
}
}
fn eval_command_subst(&mut self, pipeline: &Pipeline) -> EvalResult<Value> {
let result = self.executor.execute(pipeline, self.scope)?;
self.scope.set_last_result(result.clone());
Ok(result_to_value(&result))
}
}
pub fn value_to_exit_code(value: &Value) -> anyhow::Result<i64> {
match value {
Value::Int(n) => Ok(*n),
Value::Bool(b) => Ok(if *b { 0 } else { 1 }),
Value::Float(f) => Ok(*f as i64),
Value::String(s) => {
let trimmed = s.trim();
trimmed.parse::<i64>().map_err(|_| {
anyhow::anyhow!("numeric argument required: {:?}", s)
})
}
Value::Null | Value::Json(_) | Value::Blob(_) => {
anyhow::bail!("numeric argument required (got {:?})", value)
}
}
}
pub fn value_to_string(value: &Value) -> String {
match value {
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::String(s) => s.clone(),
Value::Json(json) => json.to_string(),
Value::Blob(blob) => format!("[blob: {} {}]", blob.formatted_size(), blob.content_type),
}
}
pub fn value_to_bool(value: &Value) -> bool {
match value {
Value::Null => false,
Value::Bool(b) => *b,
Value::Int(i) => *i != 0,
Value::Float(f) => *f != 0.0,
Value::String(s) => !s.is_empty(),
Value::Json(json) => match json {
serde_json::Value::Null => false,
serde_json::Value::Array(arr) => !arr.is_empty(),
serde_json::Value::Object(obj) => !obj.is_empty(),
serde_json::Value::Bool(b) => *b,
serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
serde_json::Value::String(s) => !s.is_empty(),
},
Value::Blob(_) => true, }
}
pub fn expand_tilde(s: &str, home: Option<&str>) -> String {
if s == "~" {
home.map(|h| h.to_string()).unwrap_or_else(|| "~".to_string())
} else if s.starts_with("~/") {
match home {
Some(home) => format!("{}{}", home, &s[1..]),
None => s.to_string(),
}
} else if s.starts_with('~') {
expand_tilde_user(s)
} else {
s.to_string()
}
}
#[cfg(all(unix, feature = "host"))]
fn expand_tilde_user(s: &str) -> String {
let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
(&s[1..slash_pos + 1], &s[slash_pos + 1..])
} else {
(&s[1..], "")
};
if username.is_empty() {
return s.to_string();
}
let passwd = match std::fs::read_to_string("/etc/passwd") {
Ok(content) => content,
Err(_) => return s.to_string(),
};
for line in passwd.lines() {
let fields: Vec<&str> = line.split(':').collect();
if fields.len() >= 6 && fields[0] == username {
let home_dir = fields[5];
return if rest.is_empty() {
home_dir.to_string()
} else {
format!("{}{}", home_dir, rest)
};
}
}
s.to_string()
}
#[cfg(not(all(unix, feature = "host")))]
fn expand_tilde_user(s: &str) -> String {
s.to_string()
}
pub fn value_to_string_with_tilde(value: &Value, home: Option<&str>) -> String {
match value {
Value::String(s) if s.starts_with('~') => expand_tilde(s, home),
_ => value_to_string(value),
}
}
fn format_path(path: &VarPath) -> String {
use crate::ast::VarSegment;
let mut result = String::from("${");
for (i, seg) in path.segments.iter().enumerate() {
match seg {
VarSegment::Field(name) => {
if i > 0 {
result.push('.');
}
result.push_str(name);
}
}
}
result.push('}');
result
}
fn is_truthy(value: &Value) -> bool {
value_to_bool(value)
}
fn values_equal(left: &Value, right: &Value) -> bool {
match (left, right) {
(Value::Null, Value::Null) => true,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Int(a), Value::Int(b)) => a == b,
(Value::Float(a), Value::Float(b)) => (a - b).abs() < f64::EPSILON,
(Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
(*a as f64 - b).abs() < f64::EPSILON
}
(Value::String(a), Value::String(b)) => a == b,
(Value::Json(a), Value::Json(b)) => a == b,
(Value::Blob(a), Value::Blob(b)) => a.id == b.id,
_ => value_to_string(left) == value_to_string(right),
}
}
fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
match (left, right) {
(Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
(Value::Float(a), Value::Float(b)) => {
a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
}
(Value::Int(a), Value::Float(b)) => {
(*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
}
(Value::Float(a), Value::Int(b)) => {
a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
}
(Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
_ => Err(EvalError::TypeError {
expected: "comparable types (numbers or strings)",
got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
}),
}
}
enum Num {
Int(i64),
Float(f64),
}
fn value_to_num(value: &Value) -> EvalResult<Num> {
match value {
Value::Int(n) => Ok(Num::Int(*n)),
Value::Float(f) => Ok(Num::Float(*f)),
Value::String(s) => {
let t = s.trim();
if let Ok(n) = t.parse::<i64>() {
Ok(Num::Int(n))
} else if let Ok(f) = t.parse::<f64>() {
Ok(Num::Float(f))
} else {
Err(EvalError::TypeError {
expected: "numeric operand",
got: format!("non-numeric string {:?}", s),
})
}
}
_ => Err(EvalError::TypeError {
expected: "numeric operand",
got: type_name(value).to_string(),
}),
}
}
fn numeric_compare(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
let l = value_to_num(left)?;
let r = value_to_num(right)?;
match (l, r) {
(Num::Int(a), Num::Int(b)) => Ok(a.cmp(&b)),
(Num::Float(a), Num::Float(b)) => a
.partial_cmp(&b)
.ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
(Num::Int(a), Num::Float(b)) => (a as f64)
.partial_cmp(&b)
.ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
(Num::Float(a), Num::Int(b)) => a
.partial_cmp(&(b as f64))
.ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
}
}
fn type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::String(_) => "string",
Value::Json(_) => "json",
Value::Blob(_) => "blob",
}
}
fn result_to_value(result: &ExecResult) -> Value {
if let Some(data) = &result.data {
return data.clone();
}
Value::String(result.text_out().trim_end().to_string())
}
fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
let text = match left {
Value::String(s) => s.as_str(),
_ => {
return Err(EvalError::TypeError {
expected: "string",
got: type_name(left).to_string(),
})
}
};
let pattern = match right {
Value::String(s) => s.as_str(),
_ => {
return Err(EvalError::TypeError {
expected: "string (regex pattern)",
got: type_name(right).to_string(),
})
}
};
let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
let matches = re.is_match(text);
Ok(Value::Bool(if negate { !matches } else { matches }))
}
pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
let mut executor = NoOpExecutor;
let mut evaluator = Evaluator::new(scope, &mut executor);
evaluator.eval(expr)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::VarSegment;
fn var_expr(name: &str) -> Expr {
Expr::VarRef(VarPath::simple(name))
}
#[test]
fn eval_literal_int() {
let mut scope = Scope::new();
let expr = Expr::Literal(Value::Int(42));
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
}
#[test]
fn eval_literal_string() {
let mut scope = Scope::new();
let expr = Expr::Literal(Value::String("hello".into()));
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
}
#[test]
fn eval_literal_bool() {
let mut scope = Scope::new();
assert_eq!(
eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
Ok(Value::Bool(true))
);
}
#[test]
fn eval_literal_null() {
let mut scope = Scope::new();
assert_eq!(
eval_expr(&Expr::Literal(Value::Null), &mut scope),
Ok(Value::Null)
);
}
#[test]
fn eval_literal_float() {
let mut scope = Scope::new();
let expr = Expr::Literal(Value::Float(3.14));
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
}
#[test]
fn eval_variable_ref() {
let mut scope = Scope::new();
scope.set("X", Value::Int(100));
assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
}
#[test]
fn eval_undefined_variable() {
let mut scope = Scope::new();
let result = eval_expr(&var_expr("MISSING"), &mut scope);
assert!(matches!(result, Err(EvalError::InvalidPath(_))));
}
#[test]
fn eval_interpolated_string() {
let mut scope = Scope::new();
scope.set("NAME", Value::String("World".into()));
let expr = Expr::Interpolated(vec![
StringPart::Literal("Hello, ".into()),
StringPart::Var(VarPath::simple("NAME")),
StringPart::Literal("!".into()),
]);
assert_eq!(
eval_expr(&expr, &mut scope),
Ok(Value::String("Hello, World!".into()))
);
}
#[test]
fn eval_interpolated_with_number() {
let mut scope = Scope::new();
scope.set("COUNT", Value::Int(42));
let expr = Expr::Interpolated(vec![
StringPart::Literal("Count: ".into()),
StringPart::Var(VarPath::simple("COUNT")),
]);
assert_eq!(
eval_expr(&expr, &mut scope),
Ok(Value::String("Count: 42".into()))
);
}
#[test]
fn eval_and_short_circuit_true() {
let mut scope = Scope::new();
let expr = Expr::BinaryOp {
left: Box::new(Expr::Literal(Value::Bool(true))),
op: BinaryOp::And,
right: Box::new(Expr::Literal(Value::Int(42))),
};
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
}
#[test]
fn eval_and_short_circuit_false() {
let mut scope = Scope::new();
let expr = Expr::BinaryOp {
left: Box::new(Expr::Literal(Value::Bool(false))),
op: BinaryOp::And,
right: Box::new(Expr::Literal(Value::Int(42))),
};
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
}
#[test]
fn eval_or_short_circuit_true() {
let mut scope = Scope::new();
let expr = Expr::BinaryOp {
left: Box::new(Expr::Literal(Value::Bool(true))),
op: BinaryOp::Or,
right: Box::new(Expr::Literal(Value::Int(42))),
};
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
}
#[test]
fn eval_or_short_circuit_false() {
let mut scope = Scope::new();
let expr = Expr::BinaryOp {
left: Box::new(Expr::Literal(Value::Bool(false))),
op: BinaryOp::Or,
right: Box::new(Expr::Literal(Value::Int(42))),
};
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
}
#[test]
fn is_truthy_values() {
assert!(!is_truthy(&Value::Null));
assert!(!is_truthy(&Value::Bool(false)));
assert!(is_truthy(&Value::Bool(true)));
assert!(!is_truthy(&Value::Int(0)));
assert!(is_truthy(&Value::Int(1)));
assert!(is_truthy(&Value::Int(-1)));
assert!(!is_truthy(&Value::Float(0.0)));
assert!(is_truthy(&Value::Float(0.1)));
assert!(!is_truthy(&Value::String("".into())));
assert!(is_truthy(&Value::String("x".into())));
}
#[test]
fn eval_command_subst_fails_without_executor() {
use crate::ast::{Command, Pipeline};
let mut scope = Scope::new();
let pipeline = Pipeline {
commands: vec![Command {
name: "echo".into(),
args: vec![],
redirects: vec![],
}],
background: false,
};
let expr = Expr::CommandSubst(Box::new(pipeline));
assert!(matches!(
eval_expr(&expr, &mut scope),
Err(EvalError::NoExecutor)
));
}
#[test]
fn eval_last_result_bare() {
let mut scope = Scope::new();
scope.set_last_result(ExecResult::failure(42, "test error"));
let expr = Expr::VarRef(VarPath {
segments: vec![VarSegment::Field("?".into())],
});
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
}
#[test]
fn value_to_string_all_types() {
assert_eq!(value_to_string(&Value::Null), "null");
assert_eq!(value_to_string(&Value::Bool(true)), "true");
assert_eq!(value_to_string(&Value::Int(42)), "42");
assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
}
#[test]
fn eval_negative_int() {
let mut scope = Scope::new();
let expr = Expr::Literal(Value::Int(-42));
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
}
#[test]
fn eval_negative_float() {
let mut scope = Scope::new();
let expr = Expr::Literal(Value::Float(-3.14));
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
}
#[test]
fn eval_zero_values() {
let mut scope = Scope::new();
assert_eq!(
eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
Ok(Value::Int(0))
);
assert_eq!(
eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
Ok(Value::Float(0.0))
);
}
#[test]
fn eval_interpolation_empty_var() {
let mut scope = Scope::new();
scope.set("EMPTY", Value::String("".into()));
let expr = Expr::Interpolated(vec![
StringPart::Literal("prefix".into()),
StringPart::Var(VarPath::simple("EMPTY")),
StringPart::Literal("suffix".into()),
]);
assert_eq!(
eval_expr(&expr, &mut scope),
Ok(Value::String("prefixsuffix".into()))
);
}
#[test]
fn eval_chained_and() {
let mut scope = Scope::new();
let expr = Expr::BinaryOp {
left: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Literal(Value::Bool(true))),
op: BinaryOp::And,
right: Box::new(Expr::Literal(Value::Bool(true))),
}),
op: BinaryOp::And,
right: Box::new(Expr::Literal(Value::Int(42))),
};
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
}
#[test]
fn eval_chained_or() {
let mut scope = Scope::new();
let expr = Expr::BinaryOp {
left: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Literal(Value::Bool(false))),
op: BinaryOp::Or,
right: Box::new(Expr::Literal(Value::Bool(false))),
}),
op: BinaryOp::Or,
right: Box::new(Expr::Literal(Value::Int(42))),
};
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
}
#[test]
fn eval_mixed_and_or() {
let mut scope = Scope::new();
let expr = Expr::BinaryOp {
left: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Literal(Value::Bool(true))),
op: BinaryOp::Or,
right: Box::new(Expr::Literal(Value::Bool(false))),
}),
op: BinaryOp::And,
right: Box::new(Expr::Literal(Value::Bool(true))),
};
assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
}
#[test]
fn eval_interpolation_with_bool() {
let mut scope = Scope::new();
scope.set("FLAG", Value::Bool(true));
let expr = Expr::Interpolated(vec![
StringPart::Literal("enabled: ".into()),
StringPart::Var(VarPath::simple("FLAG")),
]);
assert_eq!(
eval_expr(&expr, &mut scope),
Ok(Value::String("enabled: true".into()))
);
}
#[test]
fn eval_interpolation_with_null() {
let mut scope = Scope::new();
scope.set("VAL", Value::Null);
let expr = Expr::Interpolated(vec![
StringPart::Literal("value: ".into()),
StringPart::Var(VarPath::simple("VAL")),
]);
assert_eq!(
eval_expr(&expr, &mut scope),
Ok(Value::String("value: null".into()))
);
}
#[test]
fn eval_format_path_simple() {
let path = VarPath::simple("X");
assert_eq!(format_path(&path), "${X}");
}
#[test]
fn eval_format_path_nested() {
let path = VarPath {
segments: vec![
VarSegment::Field("X".into()),
VarSegment::Field("field".into()),
],
};
assert_eq!(format_path(&path), "${X.field}");
}
#[test]
fn type_name_all_types() {
assert_eq!(type_name(&Value::Null), "null");
assert_eq!(type_name(&Value::Bool(true)), "bool");
assert_eq!(type_name(&Value::Int(1)), "int");
assert_eq!(type_name(&Value::Float(1.0)), "float");
assert_eq!(type_name(&Value::String("".into())), "string");
}
#[test]
fn expand_tilde_home() {
let home = "/home/session";
assert_eq!(expand_tilde("~", Some(home)), home);
assert_eq!(expand_tilde("~/foo", Some(home)), format!("{}/foo", home));
assert_eq!(
expand_tilde("~/foo/bar", Some(home)),
format!("{}/foo/bar", home)
);
}
#[test]
fn expand_tilde_hermetic_no_home_does_not_leak_host() {
assert_eq!(expand_tilde("~", None), "~");
assert_eq!(expand_tilde("~/foo", None), "~/foo");
}
#[test]
fn expand_tilde_passthrough() {
assert_eq!(expand_tilde("/home/user", Some("/h")), "/home/user");
assert_eq!(expand_tilde("foo~bar", Some("/h")), "foo~bar");
assert_eq!(expand_tilde("", Some("/h")), "");
}
#[test]
#[cfg(all(unix, feature = "host"))]
fn expand_tilde_user() {
let expanded = expand_tilde("~root", None);
assert!(
expanded == "/root" || expanded == "/var/root",
"expected /root or /var/root, got: {}",
expanded
);
let expanded_path = expand_tilde("~root/subdir", None);
assert!(
expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
"expected /root/subdir or /var/root/subdir, got: {}",
expanded_path
);
let nonexistent = expand_tilde("~nonexistent_user_12345", None);
assert_eq!(nonexistent, "~nonexistent_user_12345");
}
#[test]
fn value_to_string_with_tilde_expansion() {
let val = Value::String("~/test".into());
assert_eq!(
value_to_string_with_tilde(&val, Some("/home/session")),
"/home/session/test"
);
}
#[test]
fn eval_positional_param() {
let mut scope = Scope::new();
scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
let expr = Expr::Positional(0);
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("my_tool".into()));
let expr = Expr::Positional(1);
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("hello".into()));
let expr = Expr::Positional(2);
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("world".into()));
let expr = Expr::Positional(3);
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("".into()));
}
#[test]
fn eval_all_args() {
let mut scope = Scope::new();
scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
let expr = Expr::AllArgs;
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("a b c".into()));
}
#[test]
fn eval_arg_count() {
let mut scope = Scope::new();
scope.set_positional("test", vec!["x".into(), "y".into()]);
let expr = Expr::ArgCount;
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::Int(2));
}
#[test]
fn eval_arg_count_empty() {
let mut scope = Scope::new();
let expr = Expr::ArgCount;
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::Int(0));
}
#[test]
fn eval_var_length_string() {
let mut scope = Scope::new();
scope.set("NAME", Value::String("hello".into()));
let expr = Expr::VarLength("NAME".into());
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::Int(5));
}
#[test]
fn eval_var_length_empty_string() {
let mut scope = Scope::new();
scope.set("EMPTY", Value::String("".into()));
let expr = Expr::VarLength("EMPTY".into());
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::Int(0));
}
#[test]
fn eval_var_length_unset() {
let mut scope = Scope::new();
let expr = Expr::VarLength("MISSING".into());
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::Int(0));
}
#[test]
fn eval_var_length_int() {
let mut scope = Scope::new();
scope.set("NUM", Value::Int(12345));
let expr = Expr::VarLength("NUM".into());
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::Int(5)); }
#[test]
fn eval_var_with_default_set() {
let mut scope = Scope::new();
scope.set("NAME", Value::String("Alice".into()));
let expr = Expr::VarWithDefault {
name: "NAME".into(),
default: vec![StringPart::Literal("default".into())],
};
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("Alice".into()));
}
#[test]
fn eval_var_with_default_unset() {
let mut scope = Scope::new();
let expr = Expr::VarWithDefault {
name: "MISSING".into(),
default: vec![StringPart::Literal("fallback".into())],
};
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("fallback".into()));
}
#[test]
fn eval_var_with_default_empty() {
let mut scope = Scope::new();
scope.set("EMPTY", Value::String("".into()));
let expr = Expr::VarWithDefault {
name: "EMPTY".into(),
default: vec![StringPart::Literal("not empty".into())],
};
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("not empty".into()));
}
#[test]
fn eval_var_with_default_non_string() {
let mut scope = Scope::new();
scope.set("NUM", Value::Int(42));
let expr = Expr::VarWithDefault {
name: "NUM".into(),
default: vec![StringPart::Literal("default".into())],
};
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::Int(42));
}
#[test]
fn eval_unset_variable_is_empty() {
let mut scope = Scope::new();
let parts = vec![
StringPart::Literal("prefix:".into()),
StringPart::Var(VarPath::simple("UNSET")),
StringPart::Literal(":suffix".into()),
];
let expr = Expr::Interpolated(parts);
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("prefix::suffix".into()));
}
#[test]
fn eval_unset_variable_multiple() {
let mut scope = Scope::new();
scope.set("SET", Value::String("hello".into()));
let parts = vec![
StringPart::Var(VarPath::simple("UNSET1")),
StringPart::Literal("-".into()),
StringPart::Var(VarPath::simple("SET")),
StringPart::Literal("-".into()),
StringPart::Var(VarPath::simple("UNSET2")),
];
let expr = Expr::Interpolated(parts);
let result = eval_expr(&expr, &mut scope).unwrap();
assert_eq!(result, Value::String("-hello-".into()));
}
}