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
39pub struct HeredocAssembler {
53 out: String,
54 strip_tabs: bool,
55 at_line_start: bool,
56}
57
58impl HeredocAssembler {
59 pub fn new(strip_tabs: bool) -> Self {
60 Self {
61 out: String::new(),
62 strip_tabs,
63 at_line_start: true,
64 }
65 }
66
67 pub fn push_literal(&mut self, literal: &str) {
70 if !self.strip_tabs {
71 self.out.push_str(literal);
72 return;
73 }
74 for ch in literal.chars() {
75 match ch {
76 '\n' => {
77 self.out.push(ch);
78 self.at_line_start = true;
79 }
80 '\t' if self.at_line_start => {} _ => {
82 self.out.push(ch);
83 self.at_line_start = false;
84 }
85 }
86 }
87 }
88
89 pub fn push_interpolated(&mut self, value: &str) {
94 self.out.push_str(value);
95 if self.strip_tabs {
96 self.at_line_start = false;
97 }
98 }
99
100 pub fn into_string(self) -> String {
101 self.out
102 }
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub enum EvalError {
108 UndefinedVariable(String),
110 InvalidPath(String),
112 TypeError { expected: &'static str, got: String },
114 CommandFailed(String),
116 NoExecutor,
118 ArithmeticError(String),
120 RegexError(String),
122}
123
124impl fmt::Display for EvalError {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 match self {
127 EvalError::UndefinedVariable(name) => write!(f, "undefined variable: {name}"),
128 EvalError::InvalidPath(path) => write!(f, "invalid path: {path}"),
129 EvalError::TypeError { expected, got } => {
130 write!(f, "type error: expected {expected}, got {got}")
131 }
132 EvalError::CommandFailed(msg) => write!(f, "command failed: {msg}"),
133 EvalError::NoExecutor => write!(f, "no executor available for command substitution"),
134 EvalError::ArithmeticError(msg) => write!(f, "arithmetic error: {msg}"),
135 EvalError::RegexError(msg) => write!(f, "regex error: {msg}"),
136 }
137 }
138}
139
140impl std::error::Error for EvalError {}
141
142pub type EvalResult<T> = Result<T, EvalError>;
144
145pub trait Executor {
151 fn execute(&mut self, stmts: &[Stmt], scope: &mut Scope) -> EvalResult<ExecResult>;
160
161 fn file_stat(&self, path: &Path) -> Option<DirEntry> {
168 std::fs::metadata(path).ok().map(|meta| {
169 if meta.is_dir() {
170 DirEntry::directory(path.file_name().unwrap_or_default().to_string_lossy())
171 } else {
172 #[allow(unused_mut)]
173 let mut entry = DirEntry::file(
174 path.file_name().unwrap_or_default().to_string_lossy(),
175 meta.len(),
176 );
177 #[cfg(unix)]
178 {
179 use std::os::unix::fs::PermissionsExt;
180 entry.permissions = Some(meta.permissions().mode());
181 }
182 entry
183 }
184 })
185 }
186}
187
188pub struct NoOpExecutor;
192
193impl Executor for NoOpExecutor {
194 fn execute(&mut self, _stmts: &[Stmt], _scope: &mut Scope) -> EvalResult<ExecResult> {
195 Err(EvalError::NoExecutor)
196 }
197}
198
199pub struct Evaluator<'a, E: Executor> {
204 scope: &'a mut Scope,
205 executor: &'a mut E,
206}
207
208impl<'a, E: Executor> Evaluator<'a, E> {
209 pub fn new(scope: &'a mut Scope, executor: &'a mut E) -> Self {
211 Self { scope, executor }
212 }
213
214 pub fn eval(&mut self, expr: &Expr) -> EvalResult<Value> {
216 match expr {
217 Expr::Literal(value) => self.eval_literal(value),
218 Expr::VarRef(path) => self.eval_var_ref(path),
219 Expr::Interpolated(parts) => self.eval_interpolated(parts),
220 Expr::HereDocBody { parts, strip_tabs } => {
221 let mut asm = HeredocAssembler::new(*strip_tabs);
224 for sp in parts {
225 match &sp.part {
226 StringPart::Literal(s) => asm.push_literal(s),
227 other => {
228 let value = self.eval_interpolated(std::slice::from_ref(other))?;
229 asm.push_interpolated(&value_to_string(&value));
230 }
231 }
232 }
233 Ok(Value::String(asm.into_string()))
234 }
235 Expr::BinaryOp { left, op, right } => self.eval_binary_op(left, *op, right),
236 Expr::CommandSubst(stmts) => self.eval_command_subst(stmts),
237 Expr::Test(test_expr) => self.eval_test(test_expr),
238 Expr::Positional(n) => self.eval_positional(*n),
239 Expr::AllArgs => self.eval_all_args(),
240 Expr::ArgCount => self.eval_arg_count(),
241 Expr::VarLength(name) => self.eval_var_length(name),
242 Expr::VarWithDefault { name, default } => self.eval_var_with_default(name, default),
243 Expr::Arithmetic(expr_str) => self.eval_arithmetic(expr_str),
244 Expr::Command(cmd) => self.eval_command(cmd),
245 Expr::LastExitCode => self.eval_last_exit_code(),
246 Expr::CurrentPid => self.eval_current_pid(),
247 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
248 }
249 }
250
251 fn eval_last_exit_code(&self) -> EvalResult<Value> {
253 Ok(Value::Int(self.scope.last_result().code))
254 }
255
256 fn eval_current_pid(&self) -> EvalResult<Value> {
258 Ok(Value::Int(self.scope.pid() as i64))
259 }
260
261 fn eval_command(&mut self, cmd: &crate::ast::Command) -> EvalResult<Value> {
263 match cmd.name.as_str() {
266 "true" => return Ok(Value::Bool(true)),
267 "false" => return Ok(Value::Bool(false)),
268 _ => {}
269 }
270
271 let block = [Stmt::Command(cmd.clone())];
273 let result = self.executor.execute(&block, self.scope)?;
274 Ok(Value::Bool(result.code == 0))
276 }
277
278 fn eval_arithmetic(&mut self, expr_str: &str) -> EvalResult<Value> {
280 arithmetic::eval_arithmetic(expr_str, self.scope)
281 .map(Value::Int)
282 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
283 }
284
285 fn eval_test(&mut self, test_expr: &TestExpr) -> EvalResult<Value> {
287 let result = match test_expr {
288 TestExpr::FileTest { op, path } => {
289 let path_value = self.eval(path)?;
290 let path_str = value_to_string(&path_value);
291 let path = Path::new(&path_str);
292 let entry = self.executor.file_stat(path);
293 match op {
294 FileTestOp::Exists => entry.is_some(),
295 FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
296 FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
297 FileTestOp::Readable => entry.is_some(),
298 FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
299 e.permissions.is_none_or(|p| p & 0o222 != 0)
300 }),
301 FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
302 e.permissions.is_some_and(|p| p & 0o111 != 0)
303 }),
304 }
305 }
306 TestExpr::StringTest { op, value } => {
307 let val = self.eval(value)?;
308 let s = value_to_string(&val);
309 match op {
310 StringTestOp::IsEmpty => s.is_empty(),
311 StringTestOp::IsNonEmpty => !s.is_empty(),
312 }
313 }
314 TestExpr::Comparison { left, op, right } => {
315 let left_val = self.eval(left)?;
316 let right_val = self.eval(right)?;
317
318 match op {
319 TestCmpOp::Eq => values_equal(&left_val, &right_val),
320 TestCmpOp::NotEq => !values_equal(&left_val, &right_val),
321 TestCmpOp::Match => {
322 match regex_match(&left_val, &right_val, false)? {
324 Value::Bool(b) => b,
325 _ => false,
326 }
327 }
328 TestCmpOp::NotMatch => {
329 match regex_match(&left_val, &right_val, true)? {
331 Value::Bool(b) => b,
332 _ => true,
333 }
334 }
335 TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
336 let ord = compare_values(&left_val, &right_val)?;
338 match op {
339 TestCmpOp::Gt => ord.is_gt(),
340 TestCmpOp::Lt => ord.is_lt(),
341 TestCmpOp::GtEq => ord.is_ge(),
342 TestCmpOp::LtEq => ord.is_le(),
343 _ => unreachable!(),
344 }
345 }
346 TestCmpOp::NumEq
347 | TestCmpOp::NumNotEq
348 | TestCmpOp::NumGt
349 | TestCmpOp::NumLt
350 | TestCmpOp::NumGtEq
351 | TestCmpOp::NumLtEq => {
352 let ord = numeric_compare(&left_val, &right_val)?;
355 match op {
356 TestCmpOp::NumEq => ord.is_eq(),
357 TestCmpOp::NumNotEq => !ord.is_eq(),
358 TestCmpOp::NumGt => ord.is_gt(),
359 TestCmpOp::NumLt => ord.is_lt(),
360 TestCmpOp::NumGtEq => ord.is_ge(),
361 TestCmpOp::NumLtEq => ord.is_le(),
362 _ => unreachable!(),
363 }
364 }
365 }
366 }
367 TestExpr::And { left, right } => {
368 let left_result = self.eval_test(left)?;
370 if !value_to_bool(&left_result) {
371 false } else {
373 value_to_bool(&self.eval_test(right)?)
374 }
375 }
376 TestExpr::Or { left, right } => {
377 let left_result = self.eval_test(left)?;
379 if value_to_bool(&left_result) {
380 true } else {
382 value_to_bool(&self.eval_test(right)?)
383 }
384 }
385 TestExpr::Not { expr } => {
386 let result = self.eval_test(expr)?;
387 !value_to_bool(&result)
388 }
389 };
390 Ok(Value::Bool(result))
391 }
392
393 fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
395 Ok(value.clone())
396 }
397
398 fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
400 self.scope
401 .resolve_path(path)
402 .ok_or_else(|| EvalError::InvalidPath(format_path(path)))
403 }
404
405 fn eval_positional(&self, n: usize) -> EvalResult<Value> {
407 match self.scope.get_positional(n) {
408 Some(s) => Ok(Value::String(s.to_string())),
409 None => Ok(Value::String(String::new())), }
411 }
412
413 fn eval_all_args(&self) -> EvalResult<Value> {
417 let args = self.scope.all_args();
418 Ok(Value::String(args.join(" ")))
419 }
420
421 fn eval_arg_count(&self) -> EvalResult<Value> {
423 Ok(Value::Int(self.scope.arg_count() as i64))
424 }
425
426 fn eval_var_length(&self, name: &str) -> EvalResult<Value> {
428 match self.scope.get(name) {
429 Some(value) => {
430 let s = value_to_string(value);
431 Ok(Value::Int(s.len() as i64))
432 }
433 None => Ok(Value::Int(0)), }
435 }
436
437 fn eval_var_with_default(&mut self, name: &str, default: &[StringPart]) -> EvalResult<Value> {
440 match self.scope.get(name) {
441 Some(value) => {
442 let s = value_to_string(value);
443 if s.is_empty() {
444 self.eval_interpolated(default)
446 } else {
447 Ok(value.clone())
448 }
449 }
450 None => {
451 self.eval_interpolated(default)
453 }
454 }
455 }
456
457 fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
459 let mut result = String::new();
460 for part in parts {
461 match part {
462 StringPart::Literal(s) => result.push_str(s),
463 StringPart::Var(path) => {
464 if let Some(value) = self.scope.resolve_path(path) {
466 result.push_str(&value_to_string(&value));
467 }
468 }
469 StringPart::VarWithDefault { name, default } => {
470 let value = self.eval_var_with_default(name, default)?;
471 result.push_str(&value_to_string(&value));
472 }
473 StringPart::VarLength(name) => {
474 let value = self.eval_var_length(name)?;
475 result.push_str(&value_to_string(&value));
476 }
477 StringPart::Positional(n) => {
478 let value = self.eval_positional(*n)?;
479 result.push_str(&value_to_string(&value));
480 }
481 StringPart::AllArgs => {
482 let value = self.eval_all_args()?;
483 result.push_str(&value_to_string(&value));
484 }
485 StringPart::ArgCount => {
486 let value = self.eval_arg_count()?;
487 result.push_str(&value_to_string(&value));
488 }
489 StringPart::Arithmetic(expr) => {
490 let value = self.eval_arithmetic_string(expr)?;
492 result.push_str(&value_to_string(&value));
493 }
494 StringPart::CommandSubst(stmts) => {
495 let value = self.eval_command_subst(stmts)?;
497 result.push_str(&value_to_string(&value));
498 }
499 StringPart::LastExitCode => {
500 result.push_str(&self.scope.last_result().code.to_string());
501 }
502 StringPart::CurrentPid => {
503 result.push_str(&self.scope.pid().to_string());
504 }
505 }
506 }
507 Ok(Value::String(result))
508 }
509
510 fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
512 arithmetic::eval_arithmetic(expr, self.scope)
514 .map(Value::Int)
515 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
516 }
517
518 fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
522 match op {
523 BinaryOp::And => {
524 let left_val = self.eval(left)?;
525 if !is_truthy(&left_val) {
526 return Ok(left_val);
527 }
528 self.eval(right)
529 }
530 BinaryOp::Or => {
531 let left_val = self.eval(left)?;
532 if is_truthy(&left_val) {
533 return Ok(left_val);
534 }
535 self.eval(right)
536 }
537 }
538 }
539
540 fn eval_command_subst(&mut self, stmts: &[Stmt]) -> EvalResult<Value> {
542 let result = self.executor.execute(stmts, self.scope)?;
543
544 self.scope.set_last_result(result.clone());
546
547 Ok(result_to_value(&result))
550 }
551}
552
553pub fn value_to_exit_code(value: &Value) -> anyhow::Result<i64> {
560 match value {
561 Value::Int(n) => Ok(*n),
562 Value::Bool(b) => Ok(if *b { 0 } else { 1 }),
563 Value::Float(f) => Ok(*f as i64),
564 Value::String(s) => {
565 let trimmed = s.trim();
566 trimmed.parse::<i64>().map_err(|_| {
567 anyhow::anyhow!("numeric argument required: {:?}", s)
568 })
569 }
570 Value::Null | Value::Json(_) | Value::Bytes(_) => {
571 anyhow::bail!("numeric argument required (got {:?})", value)
572 }
573 }
574}
575
576pub fn value_to_string(value: &Value) -> String {
577 match value {
578 Value::Null => "null".to_string(),
579 Value::Bool(b) => b.to_string(),
580 Value::Int(i) => i.to_string(),
581 Value::Float(f) => f.to_string(),
582 Value::String(s) => s.clone(),
583 Value::Json(json) => json.to_string(),
584 Value::Bytes(b) => format!("[binary: {} bytes]", b.len()),
587 }
588}
589
590pub fn value_to_bool(value: &Value) -> bool {
600 match value {
601 Value::Null => false,
602 Value::Bool(b) => *b,
603 Value::Int(i) => *i != 0,
604 Value::Float(f) => *f != 0.0,
605 Value::String(s) => !s.is_empty(),
606 Value::Json(json) => match json {
607 serde_json::Value::Null => false,
608 serde_json::Value::Array(arr) => !arr.is_empty(),
609 serde_json::Value::Object(obj) => !obj.is_empty(),
610 serde_json::Value::Bool(b) => *b,
611 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
612 serde_json::Value::String(s) => !s.is_empty(),
613 },
614 Value::Bytes(b) => !b.is_empty(), }
616}
617
618pub fn expand_tilde(s: &str, home: Option<&str>) -> String {
632 if s == "~" {
633 home.map(|h| h.to_string()).unwrap_or_else(|| "~".to_string())
634 } else if s.starts_with("~/") {
635 match home {
636 Some(home) => format!("{}{}", home, &s[1..]),
637 None => s.to_string(),
638 }
639 } else if s.starts_with('~') {
640 expand_tilde_user(s)
642 } else {
643 s.to_string()
644 }
645}
646
647#[cfg(all(unix, feature = "host"))]
652fn expand_tilde_user(s: &str) -> String {
653 let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
655 (&s[1..slash_pos + 1], &s[slash_pos + 1..])
656 } else {
657 (&s[1..], "")
658 };
659
660 if username.is_empty() {
661 return s.to_string();
662 }
663
664 let passwd = match std::fs::read_to_string("/etc/passwd") {
667 Ok(content) => content,
668 Err(_) => return s.to_string(),
669 };
670
671 for line in passwd.lines() {
672 let fields: Vec<&str> = line.split(':').collect();
673 if fields.len() >= 6 && fields[0] == username {
674 let home_dir = fields[5];
675 return if rest.is_empty() {
676 home_dir.to_string()
677 } else {
678 format!("{}{}", home_dir, rest)
679 };
680 }
681 }
682
683 s.to_string()
685}
686
687#[cfg(not(all(unix, feature = "host")))]
688fn expand_tilde_user(s: &str) -> String {
689 s.to_string()
692}
693
694pub fn value_to_string_with_tilde(value: &Value, home: Option<&str>) -> String {
699 match value {
700 Value::String(s) if s.starts_with('~') => expand_tilde(s, home),
701 _ => value_to_string(value),
702 }
703}
704
705fn format_path(path: &VarPath) -> String {
707 use crate::ast::VarSegment;
708 let mut result = String::from("${");
709 for (i, seg) in path.segments.iter().enumerate() {
710 match seg {
711 VarSegment::Field(name) => {
712 if i > 0 {
713 result.push('.');
714 }
715 result.push_str(name);
716 }
717 }
718 }
719 result.push('}');
720 result
721}
722
723fn is_truthy(value: &Value) -> bool {
733 value_to_bool(value)
735}
736
737fn values_equal(left: &Value, right: &Value) -> bool {
748 match (left, right) {
749 (Value::Null, Value::Null) => true,
750 (Value::Bool(a), Value::Bool(b)) => a == b,
751 (Value::Int(a), Value::Int(b)) => a == b,
752 (Value::Float(a), Value::Float(b)) => (a - b).abs() < f64::EPSILON,
753 (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
754 (*a as f64 - b).abs() < f64::EPSILON
755 }
756 (Value::String(a), Value::String(b)) => a == b,
757 (Value::Json(a), Value::Json(b)) => a == b,
758 (Value::Bytes(a), Value::Bytes(b)) => a == b,
759 _ => value_to_string(left) == value_to_string(right),
762 }
763}
764
765fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
767 match (left, right) {
768 (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
769 (Value::Float(a), Value::Float(b)) => {
770 a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
771 }
772 (Value::Int(a), Value::Float(b)) => {
773 (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
774 }
775 (Value::Float(a), Value::Int(b)) => {
776 a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
777 }
778 (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
779 _ => Err(EvalError::TypeError {
780 expected: "comparable types (numbers or strings)",
781 got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
782 }),
783 }
784}
785
786enum Num {
791 Int(i64),
792 Float(f64),
793}
794
795fn value_to_num(value: &Value) -> EvalResult<Num> {
796 match value {
797 Value::Int(n) => Ok(Num::Int(*n)),
798 Value::Float(f) => Ok(Num::Float(*f)),
799 Value::String(s) => {
800 let t = s.trim();
801 if let Ok(n) = t.parse::<i64>() {
802 Ok(Num::Int(n))
803 } else if let Ok(f) = t.parse::<f64>() {
804 Ok(Num::Float(f))
805 } else {
806 Err(EvalError::TypeError {
807 expected: "numeric operand",
808 got: format!("non-numeric string {:?}", s),
809 })
810 }
811 }
812 _ => Err(EvalError::TypeError {
813 expected: "numeric operand",
814 got: type_name(value).to_string(),
815 }),
816 }
817}
818
819fn numeric_compare(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
822 let l = value_to_num(left)?;
823 let r = value_to_num(right)?;
824 match (l, r) {
825 (Num::Int(a), Num::Int(b)) => Ok(a.cmp(&b)),
826 (Num::Float(a), Num::Float(b)) => a
827 .partial_cmp(&b)
828 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
829 (Num::Int(a), Num::Float(b)) => (a as f64)
830 .partial_cmp(&b)
831 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
832 (Num::Float(a), Num::Int(b)) => a
833 .partial_cmp(&(b as f64))
834 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
835 }
836}
837
838fn type_name(value: &Value) -> &'static str {
840 match value {
841 Value::Null => "null",
842 Value::Bool(_) => "bool",
843 Value::Int(_) => "int",
844 Value::Float(_) => "float",
845 Value::String(_) => "string",
846 Value::Json(_) => "json",
847 Value::Bytes(_) => "bytes",
848 }
849}
850
851fn result_to_value(result: &ExecResult) -> Value {
858 if let Some(data) = &result.data {
860 return data.clone();
861 }
862 Value::String(result.text_out().trim_end_matches('\n').to_string())
869}
870
871fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
876 let text = match left {
877 Value::String(s) => s.as_str(),
878 _ => {
879 return Err(EvalError::TypeError {
880 expected: "string",
881 got: type_name(left).to_string(),
882 })
883 }
884 };
885
886 let pattern = match right {
887 Value::String(s) => s.as_str(),
888 _ => {
889 return Err(EvalError::TypeError {
890 expected: "string (regex pattern)",
891 got: type_name(right).to_string(),
892 })
893 }
894 };
895
896 let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
897 let matches = re.is_match(text);
898
899 Ok(Value::Bool(if negate { !matches } else { matches }))
900}
901
902pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
906 let mut executor = NoOpExecutor;
907 let mut evaluator = Evaluator::new(scope, &mut executor);
908 evaluator.eval(expr)
909}
910
911#[cfg(test)]
912#[allow(clippy::approx_constant)]
913mod tests {
914 use super::*;
915 use crate::ast::VarSegment;
916
917 fn var_expr(name: &str) -> Expr {
919 Expr::VarRef(VarPath::simple(name))
920 }
921
922 #[test]
923 fn eval_literal_int() {
924 let mut scope = Scope::new();
925 let expr = Expr::Literal(Value::Int(42));
926 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
927 }
928
929 #[test]
930 fn eval_literal_string() {
931 let mut scope = Scope::new();
932 let expr = Expr::Literal(Value::String("hello".into()));
933 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
934 }
935
936 #[test]
937 fn eval_literal_bool() {
938 let mut scope = Scope::new();
939 assert_eq!(
940 eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
941 Ok(Value::Bool(true))
942 );
943 }
944
945 #[test]
946 fn eval_literal_null() {
947 let mut scope = Scope::new();
948 assert_eq!(
949 eval_expr(&Expr::Literal(Value::Null), &mut scope),
950 Ok(Value::Null)
951 );
952 }
953
954 #[test]
955 fn eval_literal_float() {
956 let mut scope = Scope::new();
957 let expr = Expr::Literal(Value::Float(3.14));
958 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
959 }
960
961 #[test]
962 fn eval_variable_ref() {
963 let mut scope = Scope::new();
964 scope.set("X", Value::Int(100));
965 assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
966 }
967
968 #[test]
969 fn eval_undefined_variable() {
970 let mut scope = Scope::new();
971 let result = eval_expr(&var_expr("MISSING"), &mut scope);
972 assert!(matches!(result, Err(EvalError::InvalidPath(_))));
973 }
974
975 #[test]
976 fn eval_interpolated_string() {
977 let mut scope = Scope::new();
978 scope.set("NAME", Value::String("World".into()));
979
980 let expr = Expr::Interpolated(vec![
981 StringPart::Literal("Hello, ".into()),
982 StringPart::Var(VarPath::simple("NAME")),
983 StringPart::Literal("!".into()),
984 ]);
985 assert_eq!(
986 eval_expr(&expr, &mut scope),
987 Ok(Value::String("Hello, World!".into()))
988 );
989 }
990
991 #[test]
992 fn eval_interpolated_with_number() {
993 let mut scope = Scope::new();
994 scope.set("COUNT", Value::Int(42));
995
996 let expr = Expr::Interpolated(vec![
997 StringPart::Literal("Count: ".into()),
998 StringPart::Var(VarPath::simple("COUNT")),
999 ]);
1000 assert_eq!(
1001 eval_expr(&expr, &mut scope),
1002 Ok(Value::String("Count: 42".into()))
1003 );
1004 }
1005
1006 #[test]
1007 fn eval_and_short_circuit_true() {
1008 let mut scope = Scope::new();
1009 let expr = Expr::BinaryOp {
1010 left: Box::new(Expr::Literal(Value::Bool(true))),
1011 op: BinaryOp::And,
1012 right: Box::new(Expr::Literal(Value::Int(42))),
1013 };
1014 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1016 }
1017
1018 #[test]
1019 fn eval_and_short_circuit_false() {
1020 let mut scope = Scope::new();
1021 let expr = Expr::BinaryOp {
1022 left: Box::new(Expr::Literal(Value::Bool(false))),
1023 op: BinaryOp::And,
1024 right: Box::new(Expr::Literal(Value::Int(42))),
1025 };
1026 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
1028 }
1029
1030 #[test]
1031 fn eval_or_short_circuit_true() {
1032 let mut scope = Scope::new();
1033 let expr = Expr::BinaryOp {
1034 left: Box::new(Expr::Literal(Value::Bool(true))),
1035 op: BinaryOp::Or,
1036 right: Box::new(Expr::Literal(Value::Int(42))),
1037 };
1038 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1040 }
1041
1042 #[test]
1043 fn eval_or_short_circuit_false() {
1044 let mut scope = Scope::new();
1045 let expr = Expr::BinaryOp {
1046 left: Box::new(Expr::Literal(Value::Bool(false))),
1047 op: BinaryOp::Or,
1048 right: Box::new(Expr::Literal(Value::Int(42))),
1049 };
1050 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1052 }
1053
1054 #[test]
1055 fn is_truthy_values() {
1056 assert!(!is_truthy(&Value::Null));
1057 assert!(!is_truthy(&Value::Bool(false)));
1058 assert!(is_truthy(&Value::Bool(true)));
1059 assert!(!is_truthy(&Value::Int(0)));
1060 assert!(is_truthy(&Value::Int(1)));
1061 assert!(is_truthy(&Value::Int(-1)));
1062 assert!(!is_truthy(&Value::Float(0.0)));
1063 assert!(is_truthy(&Value::Float(0.1)));
1064 assert!(!is_truthy(&Value::String("".into())));
1065 assert!(is_truthy(&Value::String("x".into())));
1066 }
1067
1068 #[test]
1069 fn eval_command_subst_fails_without_executor() {
1070 use crate::ast::Command;
1071
1072 let mut scope = Scope::new();
1073 let expr = Expr::CommandSubst(vec![Stmt::Command(Command {
1074 name: "echo".into(),
1075 args: vec![],
1076 redirects: vec![],
1077 })]);
1078
1079 assert!(matches!(
1080 eval_expr(&expr, &mut scope),
1081 Err(EvalError::NoExecutor)
1082 ));
1083 }
1084
1085 #[test]
1086 fn eval_last_result_bare() {
1087 let mut scope = Scope::new();
1090 scope.set_last_result(ExecResult::failure(42, "test error"));
1091
1092 let expr = Expr::VarRef(VarPath {
1093 segments: vec![VarSegment::Field("?".into())],
1094 });
1095 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1096 }
1097
1098 #[test]
1099 fn value_to_string_all_types() {
1100 assert_eq!(value_to_string(&Value::Null), "null");
1101 assert_eq!(value_to_string(&Value::Bool(true)), "true");
1102 assert_eq!(value_to_string(&Value::Int(42)), "42");
1103 assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1104 assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1105 }
1106
1107 #[test]
1110 fn eval_negative_int() {
1111 let mut scope = Scope::new();
1112 let expr = Expr::Literal(Value::Int(-42));
1113 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1114 }
1115
1116 #[test]
1117 fn eval_negative_float() {
1118 let mut scope = Scope::new();
1119 let expr = Expr::Literal(Value::Float(-3.14));
1120 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1121 }
1122
1123 #[test]
1124 fn eval_zero_values() {
1125 let mut scope = Scope::new();
1126 assert_eq!(
1127 eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1128 Ok(Value::Int(0))
1129 );
1130 assert_eq!(
1131 eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1132 Ok(Value::Float(0.0))
1133 );
1134 }
1135
1136 #[test]
1137 fn eval_interpolation_empty_var() {
1138 let mut scope = Scope::new();
1139 scope.set("EMPTY", Value::String("".into()));
1140
1141 let expr = Expr::Interpolated(vec![
1142 StringPart::Literal("prefix".into()),
1143 StringPart::Var(VarPath::simple("EMPTY")),
1144 StringPart::Literal("suffix".into()),
1145 ]);
1146 assert_eq!(
1147 eval_expr(&expr, &mut scope),
1148 Ok(Value::String("prefixsuffix".into()))
1149 );
1150 }
1151
1152 #[test]
1153 fn eval_chained_and() {
1154 let mut scope = Scope::new();
1155 let expr = Expr::BinaryOp {
1157 left: Box::new(Expr::BinaryOp {
1158 left: Box::new(Expr::Literal(Value::Bool(true))),
1159 op: BinaryOp::And,
1160 right: Box::new(Expr::Literal(Value::Bool(true))),
1161 }),
1162 op: BinaryOp::And,
1163 right: Box::new(Expr::Literal(Value::Int(42))),
1164 };
1165 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1166 }
1167
1168 #[test]
1169 fn eval_chained_or() {
1170 let mut scope = Scope::new();
1171 let expr = Expr::BinaryOp {
1173 left: Box::new(Expr::BinaryOp {
1174 left: Box::new(Expr::Literal(Value::Bool(false))),
1175 op: BinaryOp::Or,
1176 right: Box::new(Expr::Literal(Value::Bool(false))),
1177 }),
1178 op: BinaryOp::Or,
1179 right: Box::new(Expr::Literal(Value::Int(42))),
1180 };
1181 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1182 }
1183
1184 #[test]
1185 fn eval_mixed_and_or() {
1186 let mut scope = Scope::new();
1187 let expr = Expr::BinaryOp {
1190 left: Box::new(Expr::BinaryOp {
1191 left: Box::new(Expr::Literal(Value::Bool(true))),
1192 op: BinaryOp::Or,
1193 right: Box::new(Expr::Literal(Value::Bool(false))),
1194 }),
1195 op: BinaryOp::And,
1196 right: Box::new(Expr::Literal(Value::Bool(true))),
1197 };
1198 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1200 }
1201
1202 #[test]
1203 fn eval_interpolation_with_bool() {
1204 let mut scope = Scope::new();
1205 scope.set("FLAG", Value::Bool(true));
1206
1207 let expr = Expr::Interpolated(vec![
1208 StringPart::Literal("enabled: ".into()),
1209 StringPart::Var(VarPath::simple("FLAG")),
1210 ]);
1211 assert_eq!(
1212 eval_expr(&expr, &mut scope),
1213 Ok(Value::String("enabled: true".into()))
1214 );
1215 }
1216
1217 #[test]
1218 fn eval_interpolation_with_null() {
1219 let mut scope = Scope::new();
1220 scope.set("VAL", Value::Null);
1221
1222 let expr = Expr::Interpolated(vec![
1223 StringPart::Literal("value: ".into()),
1224 StringPart::Var(VarPath::simple("VAL")),
1225 ]);
1226 assert_eq!(
1227 eval_expr(&expr, &mut scope),
1228 Ok(Value::String("value: null".into()))
1229 );
1230 }
1231
1232 #[test]
1233 fn eval_format_path_simple() {
1234 let path = VarPath::simple("X");
1235 assert_eq!(format_path(&path), "${X}");
1236 }
1237
1238 #[test]
1239 fn eval_format_path_nested() {
1240 let path = VarPath {
1241 segments: vec![
1242 VarSegment::Field("X".into()),
1243 VarSegment::Field("field".into()),
1244 ],
1245 };
1246 assert_eq!(format_path(&path), "${X.field}");
1247 }
1248
1249 #[test]
1250 fn type_name_all_types() {
1251 assert_eq!(type_name(&Value::Null), "null");
1252 assert_eq!(type_name(&Value::Bool(true)), "bool");
1253 assert_eq!(type_name(&Value::Int(1)), "int");
1254 assert_eq!(type_name(&Value::Float(1.0)), "float");
1255 assert_eq!(type_name(&Value::String("".into())), "string");
1256 }
1257
1258 #[test]
1259 fn expand_tilde_home() {
1260 let home = "/home/session";
1262 assert_eq!(expand_tilde("~", Some(home)), home);
1263 assert_eq!(expand_tilde("~/foo", Some(home)), format!("{}/foo", home));
1264 assert_eq!(
1265 expand_tilde("~/foo/bar", Some(home)),
1266 format!("{}/foo/bar", home)
1267 );
1268 }
1269
1270 #[test]
1271 fn expand_tilde_hermetic_no_home_does_not_leak_host() {
1272 assert_eq!(expand_tilde("~", None), "~");
1275 assert_eq!(expand_tilde("~/foo", None), "~/foo");
1276 }
1277
1278 #[test]
1279 fn expand_tilde_passthrough() {
1280 assert_eq!(expand_tilde("/home/user", Some("/h")), "/home/user");
1282 assert_eq!(expand_tilde("foo~bar", Some("/h")), "foo~bar");
1283 assert_eq!(expand_tilde("", Some("/h")), "");
1284 }
1285
1286 #[test]
1287 #[cfg(all(unix, feature = "host"))]
1288 fn expand_tilde_user() {
1289 let expanded = expand_tilde("~root", None);
1292 assert!(
1294 expanded == "/root" || expanded == "/var/root",
1295 "expected /root or /var/root, got: {}",
1296 expanded
1297 );
1298
1299 let expanded_path = expand_tilde("~root/subdir", None);
1301 assert!(
1302 expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1303 "expected /root/subdir or /var/root/subdir, got: {}",
1304 expanded_path
1305 );
1306
1307 let nonexistent = expand_tilde("~nonexistent_user_12345", None);
1309 assert_eq!(nonexistent, "~nonexistent_user_12345");
1310 }
1311
1312 #[test]
1313 fn value_to_string_with_tilde_expansion() {
1314 let val = Value::String("~/test".into());
1316 assert_eq!(
1317 value_to_string_with_tilde(&val, Some("/home/session")),
1318 "/home/session/test"
1319 );
1320 }
1321
1322 #[test]
1323 fn eval_positional_param() {
1324 let mut scope = Scope::new();
1325 scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1326
1327 let expr = Expr::Positional(0);
1329 let result = eval_expr(&expr, &mut scope).unwrap();
1330 assert_eq!(result, Value::String("my_tool".into()));
1331
1332 let expr = Expr::Positional(1);
1334 let result = eval_expr(&expr, &mut scope).unwrap();
1335 assert_eq!(result, Value::String("hello".into()));
1336
1337 let expr = Expr::Positional(2);
1339 let result = eval_expr(&expr, &mut scope).unwrap();
1340 assert_eq!(result, Value::String("world".into()));
1341
1342 let expr = Expr::Positional(3);
1344 let result = eval_expr(&expr, &mut scope).unwrap();
1345 assert_eq!(result, Value::String("".into()));
1346 }
1347
1348 #[test]
1349 fn eval_all_args() {
1350 let mut scope = Scope::new();
1351 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1352
1353 let expr = Expr::AllArgs;
1354 let result = eval_expr(&expr, &mut scope).unwrap();
1355
1356 assert_eq!(result, Value::String("a b c".into()));
1358 }
1359
1360 #[test]
1361 fn eval_arg_count() {
1362 let mut scope = Scope::new();
1363 scope.set_positional("test", vec!["x".into(), "y".into()]);
1364
1365 let expr = Expr::ArgCount;
1366 let result = eval_expr(&expr, &mut scope).unwrap();
1367 assert_eq!(result, Value::Int(2));
1368 }
1369
1370 #[test]
1371 fn eval_arg_count_empty() {
1372 let mut scope = Scope::new();
1373
1374 let expr = Expr::ArgCount;
1375 let result = eval_expr(&expr, &mut scope).unwrap();
1376 assert_eq!(result, Value::Int(0));
1377 }
1378
1379 #[test]
1380 fn eval_var_length_string() {
1381 let mut scope = Scope::new();
1382 scope.set("NAME", Value::String("hello".into()));
1383
1384 let expr = Expr::VarLength("NAME".into());
1385 let result = eval_expr(&expr, &mut scope).unwrap();
1386 assert_eq!(result, Value::Int(5));
1387 }
1388
1389 #[test]
1390 fn eval_var_length_empty_string() {
1391 let mut scope = Scope::new();
1392 scope.set("EMPTY", Value::String("".into()));
1393
1394 let expr = Expr::VarLength("EMPTY".into());
1395 let result = eval_expr(&expr, &mut scope).unwrap();
1396 assert_eq!(result, Value::Int(0));
1397 }
1398
1399 #[test]
1400 fn eval_var_length_unset() {
1401 let mut scope = Scope::new();
1402
1403 let expr = Expr::VarLength("MISSING".into());
1405 let result = eval_expr(&expr, &mut scope).unwrap();
1406 assert_eq!(result, Value::Int(0));
1407 }
1408
1409 #[test]
1410 fn eval_var_length_int() {
1411 let mut scope = Scope::new();
1412 scope.set("NUM", Value::Int(12345));
1413
1414 let expr = Expr::VarLength("NUM".into());
1416 let result = eval_expr(&expr, &mut scope).unwrap();
1417 assert_eq!(result, Value::Int(5)); }
1419
1420 #[test]
1421 fn eval_var_with_default_set() {
1422 let mut scope = Scope::new();
1423 scope.set("NAME", Value::String("Alice".into()));
1424
1425 let expr = Expr::VarWithDefault {
1427 name: "NAME".into(),
1428 default: vec![StringPart::Literal("default".into())],
1429 };
1430 let result = eval_expr(&expr, &mut scope).unwrap();
1431 assert_eq!(result, Value::String("Alice".into()));
1432 }
1433
1434 #[test]
1435 fn eval_var_with_default_unset() {
1436 let mut scope = Scope::new();
1437
1438 let expr = Expr::VarWithDefault {
1440 name: "MISSING".into(),
1441 default: vec![StringPart::Literal("fallback".into())],
1442 };
1443 let result = eval_expr(&expr, &mut scope).unwrap();
1444 assert_eq!(result, Value::String("fallback".into()));
1445 }
1446
1447 #[test]
1448 fn eval_var_with_default_empty() {
1449 let mut scope = Scope::new();
1450 scope.set("EMPTY", Value::String("".into()));
1451
1452 let expr = Expr::VarWithDefault {
1454 name: "EMPTY".into(),
1455 default: vec![StringPart::Literal("not empty".into())],
1456 };
1457 let result = eval_expr(&expr, &mut scope).unwrap();
1458 assert_eq!(result, Value::String("not empty".into()));
1459 }
1460
1461 #[test]
1462 fn eval_var_with_default_non_string() {
1463 let mut scope = Scope::new();
1464 scope.set("NUM", Value::Int(42));
1465
1466 let expr = Expr::VarWithDefault {
1468 name: "NUM".into(),
1469 default: vec![StringPart::Literal("default".into())],
1470 };
1471 let result = eval_expr(&expr, &mut scope).unwrap();
1472 assert_eq!(result, Value::Int(42));
1473 }
1474
1475 #[test]
1476 fn eval_unset_variable_is_empty() {
1477 let mut scope = Scope::new();
1478 let parts = vec![
1479 StringPart::Literal("prefix:".into()),
1480 StringPart::Var(VarPath::simple("UNSET")),
1481 StringPart::Literal(":suffix".into()),
1482 ];
1483 let expr = Expr::Interpolated(parts);
1484 let result = eval_expr(&expr, &mut scope).unwrap();
1485 assert_eq!(result, Value::String("prefix::suffix".into()));
1486 }
1487
1488 #[test]
1489 fn eval_unset_variable_multiple() {
1490 let mut scope = Scope::new();
1491 scope.set("SET", Value::String("hello".into()));
1492 let parts = vec![
1493 StringPart::Var(VarPath::simple("UNSET1")),
1494 StringPart::Literal("-".into()),
1495 StringPart::Var(VarPath::simple("SET")),
1496 StringPart::Literal("-".into()),
1497 StringPart::Var(VarPath::simple("UNSET2")),
1498 ];
1499 let expr = Expr::Interpolated(parts);
1500 let result = eval_expr(&expr, &mut scope).unwrap();
1501 assert_eq!(result, Value::String("-hello-".into()));
1502 }
1503}