1use std::fmt;
11
12use crate::arithmetic;
13use crate::ast::{BinaryOp, Expr, FileTestOp, Stmt, StringPart, StringTestOp, TestCmpOp, TestExpr, Value, VarPath};
14use crate::vfs::DirEntry;
15use std::path::Path;
16
17use super::result::ExecResult;
18use super::scope::Scope;
19
20pub fn strip_leading_tabs(s: &str) -> String {
26 let mut out = String::with_capacity(s.len());
27 let mut at_line_start = true;
28 for ch in s.chars() {
29 if at_line_start && ch == '\t' {
30 continue;
32 }
33 out.push(ch);
34 at_line_start = ch == '\n';
35 }
36 out
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub enum EvalError {
42 UndefinedVariable(String),
44 InvalidPath(String),
46 TypeError { expected: &'static str, got: String },
48 CommandFailed(String),
50 NoExecutor,
52 ArithmeticError(String),
54 RegexError(String),
56}
57
58impl fmt::Display for EvalError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 EvalError::UndefinedVariable(name) => write!(f, "undefined variable: {name}"),
62 EvalError::InvalidPath(path) => write!(f, "invalid path: {path}"),
63 EvalError::TypeError { expected, got } => {
64 write!(f, "type error: expected {expected}, got {got}")
65 }
66 EvalError::CommandFailed(msg) => write!(f, "command failed: {msg}"),
67 EvalError::NoExecutor => write!(f, "no executor available for command substitution"),
68 EvalError::ArithmeticError(msg) => write!(f, "arithmetic error: {msg}"),
69 EvalError::RegexError(msg) => write!(f, "regex error: {msg}"),
70 }
71 }
72}
73
74impl std::error::Error for EvalError {}
75
76pub type EvalResult<T> = Result<T, EvalError>;
78
79pub trait Executor {
85 fn execute(&mut self, stmts: &[Stmt], scope: &mut Scope) -> EvalResult<ExecResult>;
94
95 fn file_stat(&self, path: &Path) -> Option<DirEntry> {
102 std::fs::metadata(path).ok().map(|meta| {
103 if meta.is_dir() {
104 DirEntry::directory(path.file_name().unwrap_or_default().to_string_lossy())
105 } else {
106 #[allow(unused_mut)]
107 let mut entry = DirEntry::file(
108 path.file_name().unwrap_or_default().to_string_lossy(),
109 meta.len(),
110 );
111 #[cfg(unix)]
112 {
113 use std::os::unix::fs::PermissionsExt;
114 entry.permissions = Some(meta.permissions().mode());
115 }
116 entry
117 }
118 })
119 }
120}
121
122pub struct NoOpExecutor;
126
127impl Executor for NoOpExecutor {
128 fn execute(&mut self, _stmts: &[Stmt], _scope: &mut Scope) -> EvalResult<ExecResult> {
129 Err(EvalError::NoExecutor)
130 }
131}
132
133pub struct Evaluator<'a, E: Executor> {
138 scope: &'a mut Scope,
139 executor: &'a mut E,
140}
141
142impl<'a, E: Executor> Evaluator<'a, E> {
143 pub fn new(scope: &'a mut Scope, executor: &'a mut E) -> Self {
145 Self { scope, executor }
146 }
147
148 pub fn eval(&mut self, expr: &Expr) -> EvalResult<Value> {
150 match expr {
151 Expr::Literal(value) => self.eval_literal(value),
152 Expr::VarRef(path) => self.eval_var_ref(path),
153 Expr::Interpolated(parts) => self.eval_interpolated(parts),
154 Expr::HereDocBody { parts, strip_tabs } => {
155 let unwrapped: Vec<StringPart> =
158 parts.iter().map(|sp| sp.part.clone()).collect();
159 let value = self.eval_interpolated(&unwrapped)?;
160 if *strip_tabs {
161 if let Value::String(s) = value {
162 Ok(Value::String(strip_leading_tabs(&s)))
163 } else {
164 Ok(value)
165 }
166 } else {
167 Ok(value)
168 }
169 }
170 Expr::BinaryOp { left, op, right } => self.eval_binary_op(left, *op, right),
171 Expr::CommandSubst(stmts) => self.eval_command_subst(stmts),
172 Expr::Test(test_expr) => self.eval_test(test_expr),
173 Expr::Positional(n) => self.eval_positional(*n),
174 Expr::AllArgs => self.eval_all_args(),
175 Expr::ArgCount => self.eval_arg_count(),
176 Expr::VarLength(name) => self.eval_var_length(name),
177 Expr::VarWithDefault { name, default } => self.eval_var_with_default(name, default),
178 Expr::Arithmetic(expr_str) => self.eval_arithmetic(expr_str),
179 Expr::Command(cmd) => self.eval_command(cmd),
180 Expr::LastExitCode => self.eval_last_exit_code(),
181 Expr::CurrentPid => self.eval_current_pid(),
182 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
183 }
184 }
185
186 fn eval_last_exit_code(&self) -> EvalResult<Value> {
188 Ok(Value::Int(self.scope.last_result().code))
189 }
190
191 fn eval_current_pid(&self) -> EvalResult<Value> {
193 Ok(Value::Int(self.scope.pid() as i64))
194 }
195
196 fn eval_command(&mut self, cmd: &crate::ast::Command) -> EvalResult<Value> {
198 match cmd.name.as_str() {
201 "true" => return Ok(Value::Bool(true)),
202 "false" => return Ok(Value::Bool(false)),
203 _ => {}
204 }
205
206 let block = [Stmt::Command(cmd.clone())];
208 let result = self.executor.execute(&block, self.scope)?;
209 Ok(Value::Bool(result.code == 0))
211 }
212
213 fn eval_arithmetic(&mut self, expr_str: &str) -> EvalResult<Value> {
215 arithmetic::eval_arithmetic(expr_str, self.scope)
216 .map(Value::Int)
217 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
218 }
219
220 fn eval_test(&mut self, test_expr: &TestExpr) -> EvalResult<Value> {
222 let result = match test_expr {
223 TestExpr::FileTest { op, path } => {
224 let path_value = self.eval(path)?;
225 let path_str = value_to_string(&path_value);
226 let path = Path::new(&path_str);
227 let entry = self.executor.file_stat(path);
228 match op {
229 FileTestOp::Exists => entry.is_some(),
230 FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
231 FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
232 FileTestOp::Readable => entry.is_some(),
233 FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
234 e.permissions.is_none_or(|p| p & 0o222 != 0)
235 }),
236 FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
237 e.permissions.is_some_and(|p| p & 0o111 != 0)
238 }),
239 }
240 }
241 TestExpr::StringTest { op, value } => {
242 let val = self.eval(value)?;
243 let s = value_to_string(&val);
244 match op {
245 StringTestOp::IsEmpty => s.is_empty(),
246 StringTestOp::IsNonEmpty => !s.is_empty(),
247 }
248 }
249 TestExpr::Comparison { left, op, right } => {
250 let left_val = self.eval(left)?;
251 let right_val = self.eval(right)?;
252
253 match op {
254 TestCmpOp::Eq => values_equal(&left_val, &right_val),
255 TestCmpOp::NotEq => !values_equal(&left_val, &right_val),
256 TestCmpOp::Match => {
257 match regex_match(&left_val, &right_val, false) {
259 Ok(Value::Bool(b)) => b,
260 Ok(_) => false,
261 Err(_) => false,
262 }
263 }
264 TestCmpOp::NotMatch => {
265 match regex_match(&left_val, &right_val, true) {
267 Ok(Value::Bool(b)) => b,
268 Ok(_) => true,
269 Err(_) => true,
270 }
271 }
272 TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
273 let ord = compare_values(&left_val, &right_val)?;
275 match op {
276 TestCmpOp::Gt => ord.is_gt(),
277 TestCmpOp::Lt => ord.is_lt(),
278 TestCmpOp::GtEq => ord.is_ge(),
279 TestCmpOp::LtEq => ord.is_le(),
280 _ => unreachable!(),
281 }
282 }
283 TestCmpOp::NumEq
284 | TestCmpOp::NumNotEq
285 | TestCmpOp::NumGt
286 | TestCmpOp::NumLt
287 | TestCmpOp::NumGtEq
288 | TestCmpOp::NumLtEq => {
289 let ord = numeric_compare(&left_val, &right_val)?;
292 match op {
293 TestCmpOp::NumEq => ord.is_eq(),
294 TestCmpOp::NumNotEq => !ord.is_eq(),
295 TestCmpOp::NumGt => ord.is_gt(),
296 TestCmpOp::NumLt => ord.is_lt(),
297 TestCmpOp::NumGtEq => ord.is_ge(),
298 TestCmpOp::NumLtEq => ord.is_le(),
299 _ => unreachable!(),
300 }
301 }
302 }
303 }
304 TestExpr::And { left, right } => {
305 let left_result = self.eval_test(left)?;
307 if !value_to_bool(&left_result) {
308 false } else {
310 value_to_bool(&self.eval_test(right)?)
311 }
312 }
313 TestExpr::Or { left, right } => {
314 let left_result = self.eval_test(left)?;
316 if value_to_bool(&left_result) {
317 true } else {
319 value_to_bool(&self.eval_test(right)?)
320 }
321 }
322 TestExpr::Not { expr } => {
323 let result = self.eval_test(expr)?;
324 !value_to_bool(&result)
325 }
326 };
327 Ok(Value::Bool(result))
328 }
329
330 fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
332 Ok(value.clone())
333 }
334
335 fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
337 self.scope
338 .resolve_path(path)
339 .ok_or_else(|| EvalError::InvalidPath(format_path(path)))
340 }
341
342 fn eval_positional(&self, n: usize) -> EvalResult<Value> {
344 match self.scope.get_positional(n) {
345 Some(s) => Ok(Value::String(s.to_string())),
346 None => Ok(Value::String(String::new())), }
348 }
349
350 fn eval_all_args(&self) -> EvalResult<Value> {
354 let args = self.scope.all_args();
355 Ok(Value::String(args.join(" ")))
356 }
357
358 fn eval_arg_count(&self) -> EvalResult<Value> {
360 Ok(Value::Int(self.scope.arg_count() as i64))
361 }
362
363 fn eval_var_length(&self, name: &str) -> EvalResult<Value> {
365 match self.scope.get(name) {
366 Some(value) => {
367 let s = value_to_string(value);
368 Ok(Value::Int(s.len() as i64))
369 }
370 None => Ok(Value::Int(0)), }
372 }
373
374 fn eval_var_with_default(&mut self, name: &str, default: &[StringPart]) -> EvalResult<Value> {
377 match self.scope.get(name) {
378 Some(value) => {
379 let s = value_to_string(value);
380 if s.is_empty() {
381 self.eval_interpolated(default)
383 } else {
384 Ok(value.clone())
385 }
386 }
387 None => {
388 self.eval_interpolated(default)
390 }
391 }
392 }
393
394 fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
396 let mut result = String::new();
397 for part in parts {
398 match part {
399 StringPart::Literal(s) => result.push_str(s),
400 StringPart::Var(path) => {
401 if let Some(value) = self.scope.resolve_path(path) {
403 result.push_str(&value_to_string(&value));
404 }
405 }
406 StringPart::VarWithDefault { name, default } => {
407 let value = self.eval_var_with_default(name, default)?;
408 result.push_str(&value_to_string(&value));
409 }
410 StringPart::VarLength(name) => {
411 let value = self.eval_var_length(name)?;
412 result.push_str(&value_to_string(&value));
413 }
414 StringPart::Positional(n) => {
415 let value = self.eval_positional(*n)?;
416 result.push_str(&value_to_string(&value));
417 }
418 StringPart::AllArgs => {
419 let value = self.eval_all_args()?;
420 result.push_str(&value_to_string(&value));
421 }
422 StringPart::ArgCount => {
423 let value = self.eval_arg_count()?;
424 result.push_str(&value_to_string(&value));
425 }
426 StringPart::Arithmetic(expr) => {
427 let value = self.eval_arithmetic_string(expr)?;
429 result.push_str(&value_to_string(&value));
430 }
431 StringPart::CommandSubst(stmts) => {
432 let value = self.eval_command_subst(stmts)?;
434 result.push_str(&value_to_string(&value));
435 }
436 StringPart::LastExitCode => {
437 result.push_str(&self.scope.last_result().code.to_string());
438 }
439 StringPart::CurrentPid => {
440 result.push_str(&self.scope.pid().to_string());
441 }
442 }
443 }
444 Ok(Value::String(result))
445 }
446
447 fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
449 arithmetic::eval_arithmetic(expr, self.scope)
451 .map(Value::Int)
452 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
453 }
454
455 fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
459 match op {
460 BinaryOp::And => {
461 let left_val = self.eval(left)?;
462 if !is_truthy(&left_val) {
463 return Ok(left_val);
464 }
465 self.eval(right)
466 }
467 BinaryOp::Or => {
468 let left_val = self.eval(left)?;
469 if is_truthy(&left_val) {
470 return Ok(left_val);
471 }
472 self.eval(right)
473 }
474 }
475 }
476
477 fn eval_command_subst(&mut self, stmts: &[Stmt]) -> EvalResult<Value> {
479 let result = self.executor.execute(stmts, self.scope)?;
480
481 self.scope.set_last_result(result.clone());
483
484 Ok(result_to_value(&result))
487 }
488}
489
490pub fn value_to_exit_code(value: &Value) -> anyhow::Result<i64> {
497 match value {
498 Value::Int(n) => Ok(*n),
499 Value::Bool(b) => Ok(if *b { 0 } else { 1 }),
500 Value::Float(f) => Ok(*f as i64),
501 Value::String(s) => {
502 let trimmed = s.trim();
503 trimmed.parse::<i64>().map_err(|_| {
504 anyhow::anyhow!("numeric argument required: {:?}", s)
505 })
506 }
507 Value::Null | Value::Json(_) | Value::Bytes(_) => {
508 anyhow::bail!("numeric argument required (got {:?})", value)
509 }
510 }
511}
512
513pub fn value_to_string(value: &Value) -> String {
514 match value {
515 Value::Null => "null".to_string(),
516 Value::Bool(b) => b.to_string(),
517 Value::Int(i) => i.to_string(),
518 Value::Float(f) => f.to_string(),
519 Value::String(s) => s.clone(),
520 Value::Json(json) => json.to_string(),
521 Value::Bytes(b) => format!("[binary: {} bytes]", b.len()),
524 }
525}
526
527pub fn value_to_bool(value: &Value) -> bool {
537 match value {
538 Value::Null => false,
539 Value::Bool(b) => *b,
540 Value::Int(i) => *i != 0,
541 Value::Float(f) => *f != 0.0,
542 Value::String(s) => !s.is_empty(),
543 Value::Json(json) => match json {
544 serde_json::Value::Null => false,
545 serde_json::Value::Array(arr) => !arr.is_empty(),
546 serde_json::Value::Object(obj) => !obj.is_empty(),
547 serde_json::Value::Bool(b) => *b,
548 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
549 serde_json::Value::String(s) => !s.is_empty(),
550 },
551 Value::Bytes(b) => !b.is_empty(), }
553}
554
555pub fn expand_tilde(s: &str, home: Option<&str>) -> String {
569 if s == "~" {
570 home.map(|h| h.to_string()).unwrap_or_else(|| "~".to_string())
571 } else if s.starts_with("~/") {
572 match home {
573 Some(home) => format!("{}{}", home, &s[1..]),
574 None => s.to_string(),
575 }
576 } else if s.starts_with('~') {
577 expand_tilde_user(s)
579 } else {
580 s.to_string()
581 }
582}
583
584#[cfg(all(unix, feature = "host"))]
589fn expand_tilde_user(s: &str) -> String {
590 let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
592 (&s[1..slash_pos + 1], &s[slash_pos + 1..])
593 } else {
594 (&s[1..], "")
595 };
596
597 if username.is_empty() {
598 return s.to_string();
599 }
600
601 let passwd = match std::fs::read_to_string("/etc/passwd") {
604 Ok(content) => content,
605 Err(_) => return s.to_string(),
606 };
607
608 for line in passwd.lines() {
609 let fields: Vec<&str> = line.split(':').collect();
610 if fields.len() >= 6 && fields[0] == username {
611 let home_dir = fields[5];
612 return if rest.is_empty() {
613 home_dir.to_string()
614 } else {
615 format!("{}{}", home_dir, rest)
616 };
617 }
618 }
619
620 s.to_string()
622}
623
624#[cfg(not(all(unix, feature = "host")))]
625fn expand_tilde_user(s: &str) -> String {
626 s.to_string()
629}
630
631pub fn value_to_string_with_tilde(value: &Value, home: Option<&str>) -> String {
636 match value {
637 Value::String(s) if s.starts_with('~') => expand_tilde(s, home),
638 _ => value_to_string(value),
639 }
640}
641
642fn format_path(path: &VarPath) -> String {
644 use crate::ast::VarSegment;
645 let mut result = String::from("${");
646 for (i, seg) in path.segments.iter().enumerate() {
647 match seg {
648 VarSegment::Field(name) => {
649 if i > 0 {
650 result.push('.');
651 }
652 result.push_str(name);
653 }
654 }
655 }
656 result.push('}');
657 result
658}
659
660fn is_truthy(value: &Value) -> bool {
670 value_to_bool(value)
672}
673
674fn values_equal(left: &Value, right: &Value) -> bool {
685 match (left, right) {
686 (Value::Null, Value::Null) => true,
687 (Value::Bool(a), Value::Bool(b)) => a == b,
688 (Value::Int(a), Value::Int(b)) => a == b,
689 (Value::Float(a), Value::Float(b)) => (a - b).abs() < f64::EPSILON,
690 (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
691 (*a as f64 - b).abs() < f64::EPSILON
692 }
693 (Value::String(a), Value::String(b)) => a == b,
694 (Value::Json(a), Value::Json(b)) => a == b,
695 (Value::Bytes(a), Value::Bytes(b)) => a == b,
696 _ => value_to_string(left) == value_to_string(right),
699 }
700}
701
702fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
704 match (left, right) {
705 (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
706 (Value::Float(a), Value::Float(b)) => {
707 a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
708 }
709 (Value::Int(a), Value::Float(b)) => {
710 (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
711 }
712 (Value::Float(a), Value::Int(b)) => {
713 a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
714 }
715 (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
716 _ => Err(EvalError::TypeError {
717 expected: "comparable types (numbers or strings)",
718 got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
719 }),
720 }
721}
722
723enum Num {
728 Int(i64),
729 Float(f64),
730}
731
732fn value_to_num(value: &Value) -> EvalResult<Num> {
733 match value {
734 Value::Int(n) => Ok(Num::Int(*n)),
735 Value::Float(f) => Ok(Num::Float(*f)),
736 Value::String(s) => {
737 let t = s.trim();
738 if let Ok(n) = t.parse::<i64>() {
739 Ok(Num::Int(n))
740 } else if let Ok(f) = t.parse::<f64>() {
741 Ok(Num::Float(f))
742 } else {
743 Err(EvalError::TypeError {
744 expected: "numeric operand",
745 got: format!("non-numeric string {:?}", s),
746 })
747 }
748 }
749 _ => Err(EvalError::TypeError {
750 expected: "numeric operand",
751 got: type_name(value).to_string(),
752 }),
753 }
754}
755
756fn numeric_compare(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
759 let l = value_to_num(left)?;
760 let r = value_to_num(right)?;
761 match (l, r) {
762 (Num::Int(a), Num::Int(b)) => Ok(a.cmp(&b)),
763 (Num::Float(a), Num::Float(b)) => a
764 .partial_cmp(&b)
765 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
766 (Num::Int(a), Num::Float(b)) => (a as f64)
767 .partial_cmp(&b)
768 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
769 (Num::Float(a), Num::Int(b)) => a
770 .partial_cmp(&(b as f64))
771 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
772 }
773}
774
775fn type_name(value: &Value) -> &'static str {
777 match value {
778 Value::Null => "null",
779 Value::Bool(_) => "bool",
780 Value::Int(_) => "int",
781 Value::Float(_) => "float",
782 Value::String(_) => "string",
783 Value::Json(_) => "json",
784 Value::Bytes(_) => "bytes",
785 }
786}
787
788fn result_to_value(result: &ExecResult) -> Value {
795 if let Some(data) = &result.data {
797 return data.clone();
798 }
799 Value::String(result.text_out().trim_end().to_string())
801}
802
803fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
808 let text = match left {
809 Value::String(s) => s.as_str(),
810 _ => {
811 return Err(EvalError::TypeError {
812 expected: "string",
813 got: type_name(left).to_string(),
814 })
815 }
816 };
817
818 let pattern = match right {
819 Value::String(s) => s.as_str(),
820 _ => {
821 return Err(EvalError::TypeError {
822 expected: "string (regex pattern)",
823 got: type_name(right).to_string(),
824 })
825 }
826 };
827
828 let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
829 let matches = re.is_match(text);
830
831 Ok(Value::Bool(if negate { !matches } else { matches }))
832}
833
834pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
838 let mut executor = NoOpExecutor;
839 let mut evaluator = Evaluator::new(scope, &mut executor);
840 evaluator.eval(expr)
841}
842
843#[cfg(test)]
844#[allow(clippy::approx_constant)]
845mod tests {
846 use super::*;
847 use crate::ast::VarSegment;
848
849 fn var_expr(name: &str) -> Expr {
851 Expr::VarRef(VarPath::simple(name))
852 }
853
854 #[test]
855 fn eval_literal_int() {
856 let mut scope = Scope::new();
857 let expr = Expr::Literal(Value::Int(42));
858 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
859 }
860
861 #[test]
862 fn eval_literal_string() {
863 let mut scope = Scope::new();
864 let expr = Expr::Literal(Value::String("hello".into()));
865 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
866 }
867
868 #[test]
869 fn eval_literal_bool() {
870 let mut scope = Scope::new();
871 assert_eq!(
872 eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
873 Ok(Value::Bool(true))
874 );
875 }
876
877 #[test]
878 fn eval_literal_null() {
879 let mut scope = Scope::new();
880 assert_eq!(
881 eval_expr(&Expr::Literal(Value::Null), &mut scope),
882 Ok(Value::Null)
883 );
884 }
885
886 #[test]
887 fn eval_literal_float() {
888 let mut scope = Scope::new();
889 let expr = Expr::Literal(Value::Float(3.14));
890 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
891 }
892
893 #[test]
894 fn eval_variable_ref() {
895 let mut scope = Scope::new();
896 scope.set("X", Value::Int(100));
897 assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
898 }
899
900 #[test]
901 fn eval_undefined_variable() {
902 let mut scope = Scope::new();
903 let result = eval_expr(&var_expr("MISSING"), &mut scope);
904 assert!(matches!(result, Err(EvalError::InvalidPath(_))));
905 }
906
907 #[test]
908 fn eval_interpolated_string() {
909 let mut scope = Scope::new();
910 scope.set("NAME", Value::String("World".into()));
911
912 let expr = Expr::Interpolated(vec![
913 StringPart::Literal("Hello, ".into()),
914 StringPart::Var(VarPath::simple("NAME")),
915 StringPart::Literal("!".into()),
916 ]);
917 assert_eq!(
918 eval_expr(&expr, &mut scope),
919 Ok(Value::String("Hello, World!".into()))
920 );
921 }
922
923 #[test]
924 fn eval_interpolated_with_number() {
925 let mut scope = Scope::new();
926 scope.set("COUNT", Value::Int(42));
927
928 let expr = Expr::Interpolated(vec![
929 StringPart::Literal("Count: ".into()),
930 StringPart::Var(VarPath::simple("COUNT")),
931 ]);
932 assert_eq!(
933 eval_expr(&expr, &mut scope),
934 Ok(Value::String("Count: 42".into()))
935 );
936 }
937
938 #[test]
939 fn eval_and_short_circuit_true() {
940 let mut scope = Scope::new();
941 let expr = Expr::BinaryOp {
942 left: Box::new(Expr::Literal(Value::Bool(true))),
943 op: BinaryOp::And,
944 right: Box::new(Expr::Literal(Value::Int(42))),
945 };
946 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
948 }
949
950 #[test]
951 fn eval_and_short_circuit_false() {
952 let mut scope = Scope::new();
953 let expr = Expr::BinaryOp {
954 left: Box::new(Expr::Literal(Value::Bool(false))),
955 op: BinaryOp::And,
956 right: Box::new(Expr::Literal(Value::Int(42))),
957 };
958 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
960 }
961
962 #[test]
963 fn eval_or_short_circuit_true() {
964 let mut scope = Scope::new();
965 let expr = Expr::BinaryOp {
966 left: Box::new(Expr::Literal(Value::Bool(true))),
967 op: BinaryOp::Or,
968 right: Box::new(Expr::Literal(Value::Int(42))),
969 };
970 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
972 }
973
974 #[test]
975 fn eval_or_short_circuit_false() {
976 let mut scope = Scope::new();
977 let expr = Expr::BinaryOp {
978 left: Box::new(Expr::Literal(Value::Bool(false))),
979 op: BinaryOp::Or,
980 right: Box::new(Expr::Literal(Value::Int(42))),
981 };
982 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
984 }
985
986 #[test]
987 fn is_truthy_values() {
988 assert!(!is_truthy(&Value::Null));
989 assert!(!is_truthy(&Value::Bool(false)));
990 assert!(is_truthy(&Value::Bool(true)));
991 assert!(!is_truthy(&Value::Int(0)));
992 assert!(is_truthy(&Value::Int(1)));
993 assert!(is_truthy(&Value::Int(-1)));
994 assert!(!is_truthy(&Value::Float(0.0)));
995 assert!(is_truthy(&Value::Float(0.1)));
996 assert!(!is_truthy(&Value::String("".into())));
997 assert!(is_truthy(&Value::String("x".into())));
998 }
999
1000 #[test]
1001 fn eval_command_subst_fails_without_executor() {
1002 use crate::ast::Command;
1003
1004 let mut scope = Scope::new();
1005 let expr = Expr::CommandSubst(vec![Stmt::Command(Command {
1006 name: "echo".into(),
1007 args: vec![],
1008 redirects: vec![],
1009 })]);
1010
1011 assert!(matches!(
1012 eval_expr(&expr, &mut scope),
1013 Err(EvalError::NoExecutor)
1014 ));
1015 }
1016
1017 #[test]
1018 fn eval_last_result_bare() {
1019 let mut scope = Scope::new();
1022 scope.set_last_result(ExecResult::failure(42, "test error"));
1023
1024 let expr = Expr::VarRef(VarPath {
1025 segments: vec![VarSegment::Field("?".into())],
1026 });
1027 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1028 }
1029
1030 #[test]
1031 fn value_to_string_all_types() {
1032 assert_eq!(value_to_string(&Value::Null), "null");
1033 assert_eq!(value_to_string(&Value::Bool(true)), "true");
1034 assert_eq!(value_to_string(&Value::Int(42)), "42");
1035 assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1036 assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1037 }
1038
1039 #[test]
1042 fn eval_negative_int() {
1043 let mut scope = Scope::new();
1044 let expr = Expr::Literal(Value::Int(-42));
1045 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1046 }
1047
1048 #[test]
1049 fn eval_negative_float() {
1050 let mut scope = Scope::new();
1051 let expr = Expr::Literal(Value::Float(-3.14));
1052 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1053 }
1054
1055 #[test]
1056 fn eval_zero_values() {
1057 let mut scope = Scope::new();
1058 assert_eq!(
1059 eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1060 Ok(Value::Int(0))
1061 );
1062 assert_eq!(
1063 eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1064 Ok(Value::Float(0.0))
1065 );
1066 }
1067
1068 #[test]
1069 fn eval_interpolation_empty_var() {
1070 let mut scope = Scope::new();
1071 scope.set("EMPTY", Value::String("".into()));
1072
1073 let expr = Expr::Interpolated(vec![
1074 StringPart::Literal("prefix".into()),
1075 StringPart::Var(VarPath::simple("EMPTY")),
1076 StringPart::Literal("suffix".into()),
1077 ]);
1078 assert_eq!(
1079 eval_expr(&expr, &mut scope),
1080 Ok(Value::String("prefixsuffix".into()))
1081 );
1082 }
1083
1084 #[test]
1085 fn eval_chained_and() {
1086 let mut scope = Scope::new();
1087 let expr = Expr::BinaryOp {
1089 left: Box::new(Expr::BinaryOp {
1090 left: Box::new(Expr::Literal(Value::Bool(true))),
1091 op: BinaryOp::And,
1092 right: Box::new(Expr::Literal(Value::Bool(true))),
1093 }),
1094 op: BinaryOp::And,
1095 right: Box::new(Expr::Literal(Value::Int(42))),
1096 };
1097 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1098 }
1099
1100 #[test]
1101 fn eval_chained_or() {
1102 let mut scope = Scope::new();
1103 let expr = Expr::BinaryOp {
1105 left: Box::new(Expr::BinaryOp {
1106 left: Box::new(Expr::Literal(Value::Bool(false))),
1107 op: BinaryOp::Or,
1108 right: Box::new(Expr::Literal(Value::Bool(false))),
1109 }),
1110 op: BinaryOp::Or,
1111 right: Box::new(Expr::Literal(Value::Int(42))),
1112 };
1113 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1114 }
1115
1116 #[test]
1117 fn eval_mixed_and_or() {
1118 let mut scope = Scope::new();
1119 let expr = Expr::BinaryOp {
1122 left: Box::new(Expr::BinaryOp {
1123 left: Box::new(Expr::Literal(Value::Bool(true))),
1124 op: BinaryOp::Or,
1125 right: Box::new(Expr::Literal(Value::Bool(false))),
1126 }),
1127 op: BinaryOp::And,
1128 right: Box::new(Expr::Literal(Value::Bool(true))),
1129 };
1130 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1132 }
1133
1134 #[test]
1135 fn eval_interpolation_with_bool() {
1136 let mut scope = Scope::new();
1137 scope.set("FLAG", Value::Bool(true));
1138
1139 let expr = Expr::Interpolated(vec![
1140 StringPart::Literal("enabled: ".into()),
1141 StringPart::Var(VarPath::simple("FLAG")),
1142 ]);
1143 assert_eq!(
1144 eval_expr(&expr, &mut scope),
1145 Ok(Value::String("enabled: true".into()))
1146 );
1147 }
1148
1149 #[test]
1150 fn eval_interpolation_with_null() {
1151 let mut scope = Scope::new();
1152 scope.set("VAL", Value::Null);
1153
1154 let expr = Expr::Interpolated(vec![
1155 StringPart::Literal("value: ".into()),
1156 StringPart::Var(VarPath::simple("VAL")),
1157 ]);
1158 assert_eq!(
1159 eval_expr(&expr, &mut scope),
1160 Ok(Value::String("value: null".into()))
1161 );
1162 }
1163
1164 #[test]
1165 fn eval_format_path_simple() {
1166 let path = VarPath::simple("X");
1167 assert_eq!(format_path(&path), "${X}");
1168 }
1169
1170 #[test]
1171 fn eval_format_path_nested() {
1172 let path = VarPath {
1173 segments: vec![
1174 VarSegment::Field("X".into()),
1175 VarSegment::Field("field".into()),
1176 ],
1177 };
1178 assert_eq!(format_path(&path), "${X.field}");
1179 }
1180
1181 #[test]
1182 fn type_name_all_types() {
1183 assert_eq!(type_name(&Value::Null), "null");
1184 assert_eq!(type_name(&Value::Bool(true)), "bool");
1185 assert_eq!(type_name(&Value::Int(1)), "int");
1186 assert_eq!(type_name(&Value::Float(1.0)), "float");
1187 assert_eq!(type_name(&Value::String("".into())), "string");
1188 }
1189
1190 #[test]
1191 fn expand_tilde_home() {
1192 let home = "/home/session";
1194 assert_eq!(expand_tilde("~", Some(home)), home);
1195 assert_eq!(expand_tilde("~/foo", Some(home)), format!("{}/foo", home));
1196 assert_eq!(
1197 expand_tilde("~/foo/bar", Some(home)),
1198 format!("{}/foo/bar", home)
1199 );
1200 }
1201
1202 #[test]
1203 fn expand_tilde_hermetic_no_home_does_not_leak_host() {
1204 assert_eq!(expand_tilde("~", None), "~");
1207 assert_eq!(expand_tilde("~/foo", None), "~/foo");
1208 }
1209
1210 #[test]
1211 fn expand_tilde_passthrough() {
1212 assert_eq!(expand_tilde("/home/user", Some("/h")), "/home/user");
1214 assert_eq!(expand_tilde("foo~bar", Some("/h")), "foo~bar");
1215 assert_eq!(expand_tilde("", Some("/h")), "");
1216 }
1217
1218 #[test]
1219 #[cfg(all(unix, feature = "host"))]
1220 fn expand_tilde_user() {
1221 let expanded = expand_tilde("~root", None);
1224 assert!(
1226 expanded == "/root" || expanded == "/var/root",
1227 "expected /root or /var/root, got: {}",
1228 expanded
1229 );
1230
1231 let expanded_path = expand_tilde("~root/subdir", None);
1233 assert!(
1234 expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1235 "expected /root/subdir or /var/root/subdir, got: {}",
1236 expanded_path
1237 );
1238
1239 let nonexistent = expand_tilde("~nonexistent_user_12345", None);
1241 assert_eq!(nonexistent, "~nonexistent_user_12345");
1242 }
1243
1244 #[test]
1245 fn value_to_string_with_tilde_expansion() {
1246 let val = Value::String("~/test".into());
1248 assert_eq!(
1249 value_to_string_with_tilde(&val, Some("/home/session")),
1250 "/home/session/test"
1251 );
1252 }
1253
1254 #[test]
1255 fn eval_positional_param() {
1256 let mut scope = Scope::new();
1257 scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1258
1259 let expr = Expr::Positional(0);
1261 let result = eval_expr(&expr, &mut scope).unwrap();
1262 assert_eq!(result, Value::String("my_tool".into()));
1263
1264 let expr = Expr::Positional(1);
1266 let result = eval_expr(&expr, &mut scope).unwrap();
1267 assert_eq!(result, Value::String("hello".into()));
1268
1269 let expr = Expr::Positional(2);
1271 let result = eval_expr(&expr, &mut scope).unwrap();
1272 assert_eq!(result, Value::String("world".into()));
1273
1274 let expr = Expr::Positional(3);
1276 let result = eval_expr(&expr, &mut scope).unwrap();
1277 assert_eq!(result, Value::String("".into()));
1278 }
1279
1280 #[test]
1281 fn eval_all_args() {
1282 let mut scope = Scope::new();
1283 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1284
1285 let expr = Expr::AllArgs;
1286 let result = eval_expr(&expr, &mut scope).unwrap();
1287
1288 assert_eq!(result, Value::String("a b c".into()));
1290 }
1291
1292 #[test]
1293 fn eval_arg_count() {
1294 let mut scope = Scope::new();
1295 scope.set_positional("test", vec!["x".into(), "y".into()]);
1296
1297 let expr = Expr::ArgCount;
1298 let result = eval_expr(&expr, &mut scope).unwrap();
1299 assert_eq!(result, Value::Int(2));
1300 }
1301
1302 #[test]
1303 fn eval_arg_count_empty() {
1304 let mut scope = Scope::new();
1305
1306 let expr = Expr::ArgCount;
1307 let result = eval_expr(&expr, &mut scope).unwrap();
1308 assert_eq!(result, Value::Int(0));
1309 }
1310
1311 #[test]
1312 fn eval_var_length_string() {
1313 let mut scope = Scope::new();
1314 scope.set("NAME", Value::String("hello".into()));
1315
1316 let expr = Expr::VarLength("NAME".into());
1317 let result = eval_expr(&expr, &mut scope).unwrap();
1318 assert_eq!(result, Value::Int(5));
1319 }
1320
1321 #[test]
1322 fn eval_var_length_empty_string() {
1323 let mut scope = Scope::new();
1324 scope.set("EMPTY", Value::String("".into()));
1325
1326 let expr = Expr::VarLength("EMPTY".into());
1327 let result = eval_expr(&expr, &mut scope).unwrap();
1328 assert_eq!(result, Value::Int(0));
1329 }
1330
1331 #[test]
1332 fn eval_var_length_unset() {
1333 let mut scope = Scope::new();
1334
1335 let expr = Expr::VarLength("MISSING".into());
1337 let result = eval_expr(&expr, &mut scope).unwrap();
1338 assert_eq!(result, Value::Int(0));
1339 }
1340
1341 #[test]
1342 fn eval_var_length_int() {
1343 let mut scope = Scope::new();
1344 scope.set("NUM", Value::Int(12345));
1345
1346 let expr = Expr::VarLength("NUM".into());
1348 let result = eval_expr(&expr, &mut scope).unwrap();
1349 assert_eq!(result, Value::Int(5)); }
1351
1352 #[test]
1353 fn eval_var_with_default_set() {
1354 let mut scope = Scope::new();
1355 scope.set("NAME", Value::String("Alice".into()));
1356
1357 let expr = Expr::VarWithDefault {
1359 name: "NAME".into(),
1360 default: vec![StringPart::Literal("default".into())],
1361 };
1362 let result = eval_expr(&expr, &mut scope).unwrap();
1363 assert_eq!(result, Value::String("Alice".into()));
1364 }
1365
1366 #[test]
1367 fn eval_var_with_default_unset() {
1368 let mut scope = Scope::new();
1369
1370 let expr = Expr::VarWithDefault {
1372 name: "MISSING".into(),
1373 default: vec![StringPart::Literal("fallback".into())],
1374 };
1375 let result = eval_expr(&expr, &mut scope).unwrap();
1376 assert_eq!(result, Value::String("fallback".into()));
1377 }
1378
1379 #[test]
1380 fn eval_var_with_default_empty() {
1381 let mut scope = Scope::new();
1382 scope.set("EMPTY", Value::String("".into()));
1383
1384 let expr = Expr::VarWithDefault {
1386 name: "EMPTY".into(),
1387 default: vec![StringPart::Literal("not empty".into())],
1388 };
1389 let result = eval_expr(&expr, &mut scope).unwrap();
1390 assert_eq!(result, Value::String("not empty".into()));
1391 }
1392
1393 #[test]
1394 fn eval_var_with_default_non_string() {
1395 let mut scope = Scope::new();
1396 scope.set("NUM", Value::Int(42));
1397
1398 let expr = Expr::VarWithDefault {
1400 name: "NUM".into(),
1401 default: vec![StringPart::Literal("default".into())],
1402 };
1403 let result = eval_expr(&expr, &mut scope).unwrap();
1404 assert_eq!(result, Value::Int(42));
1405 }
1406
1407 #[test]
1408 fn eval_unset_variable_is_empty() {
1409 let mut scope = Scope::new();
1410 let parts = vec![
1411 StringPart::Literal("prefix:".into()),
1412 StringPart::Var(VarPath::simple("UNSET")),
1413 StringPart::Literal(":suffix".into()),
1414 ];
1415 let expr = Expr::Interpolated(parts);
1416 let result = eval_expr(&expr, &mut scope).unwrap();
1417 assert_eq!(result, Value::String("prefix::suffix".into()));
1418 }
1419
1420 #[test]
1421 fn eval_unset_variable_multiple() {
1422 let mut scope = Scope::new();
1423 scope.set("SET", Value::String("hello".into()));
1424 let parts = vec![
1425 StringPart::Var(VarPath::simple("UNSET1")),
1426 StringPart::Literal("-".into()),
1427 StringPart::Var(VarPath::simple("SET")),
1428 StringPart::Literal("-".into()),
1429 StringPart::Var(VarPath::simple("UNSET2")),
1430 ];
1431 let expr = Expr::Interpolated(parts);
1432 let result = eval_expr(&expr, &mut scope).unwrap();
1433 assert_eq!(result, Value::String("-hello-".into()));
1434 }
1435}