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 Value::Bool(b) => b,
260 _ => false,
261 }
262 }
263 TestCmpOp::NotMatch => {
264 match regex_match(&left_val, &right_val, true)? {
266 Value::Bool(b) => b,
267 _ => true,
268 }
269 }
270 TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
271 let ord = compare_values(&left_val, &right_val)?;
273 match op {
274 TestCmpOp::Gt => ord.is_gt(),
275 TestCmpOp::Lt => ord.is_lt(),
276 TestCmpOp::GtEq => ord.is_ge(),
277 TestCmpOp::LtEq => ord.is_le(),
278 _ => unreachable!(),
279 }
280 }
281 TestCmpOp::NumEq
282 | TestCmpOp::NumNotEq
283 | TestCmpOp::NumGt
284 | TestCmpOp::NumLt
285 | TestCmpOp::NumGtEq
286 | TestCmpOp::NumLtEq => {
287 let ord = numeric_compare(&left_val, &right_val)?;
290 match op {
291 TestCmpOp::NumEq => ord.is_eq(),
292 TestCmpOp::NumNotEq => !ord.is_eq(),
293 TestCmpOp::NumGt => ord.is_gt(),
294 TestCmpOp::NumLt => ord.is_lt(),
295 TestCmpOp::NumGtEq => ord.is_ge(),
296 TestCmpOp::NumLtEq => ord.is_le(),
297 _ => unreachable!(),
298 }
299 }
300 }
301 }
302 TestExpr::And { left, right } => {
303 let left_result = self.eval_test(left)?;
305 if !value_to_bool(&left_result) {
306 false } else {
308 value_to_bool(&self.eval_test(right)?)
309 }
310 }
311 TestExpr::Or { left, right } => {
312 let left_result = self.eval_test(left)?;
314 if value_to_bool(&left_result) {
315 true } else {
317 value_to_bool(&self.eval_test(right)?)
318 }
319 }
320 TestExpr::Not { expr } => {
321 let result = self.eval_test(expr)?;
322 !value_to_bool(&result)
323 }
324 };
325 Ok(Value::Bool(result))
326 }
327
328 fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
330 Ok(value.clone())
331 }
332
333 fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
335 self.scope
336 .resolve_path(path)
337 .ok_or_else(|| EvalError::InvalidPath(format_path(path)))
338 }
339
340 fn eval_positional(&self, n: usize) -> EvalResult<Value> {
342 match self.scope.get_positional(n) {
343 Some(s) => Ok(Value::String(s.to_string())),
344 None => Ok(Value::String(String::new())), }
346 }
347
348 fn eval_all_args(&self) -> EvalResult<Value> {
352 let args = self.scope.all_args();
353 Ok(Value::String(args.join(" ")))
354 }
355
356 fn eval_arg_count(&self) -> EvalResult<Value> {
358 Ok(Value::Int(self.scope.arg_count() as i64))
359 }
360
361 fn eval_var_length(&self, name: &str) -> EvalResult<Value> {
363 match self.scope.get(name) {
364 Some(value) => {
365 let s = value_to_string(value);
366 Ok(Value::Int(s.len() as i64))
367 }
368 None => Ok(Value::Int(0)), }
370 }
371
372 fn eval_var_with_default(&mut self, name: &str, default: &[StringPart]) -> EvalResult<Value> {
375 match self.scope.get(name) {
376 Some(value) => {
377 let s = value_to_string(value);
378 if s.is_empty() {
379 self.eval_interpolated(default)
381 } else {
382 Ok(value.clone())
383 }
384 }
385 None => {
386 self.eval_interpolated(default)
388 }
389 }
390 }
391
392 fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
394 let mut result = String::new();
395 for part in parts {
396 match part {
397 StringPart::Literal(s) => result.push_str(s),
398 StringPart::Var(path) => {
399 if let Some(value) = self.scope.resolve_path(path) {
401 result.push_str(&value_to_string(&value));
402 }
403 }
404 StringPart::VarWithDefault { name, default } => {
405 let value = self.eval_var_with_default(name, default)?;
406 result.push_str(&value_to_string(&value));
407 }
408 StringPart::VarLength(name) => {
409 let value = self.eval_var_length(name)?;
410 result.push_str(&value_to_string(&value));
411 }
412 StringPart::Positional(n) => {
413 let value = self.eval_positional(*n)?;
414 result.push_str(&value_to_string(&value));
415 }
416 StringPart::AllArgs => {
417 let value = self.eval_all_args()?;
418 result.push_str(&value_to_string(&value));
419 }
420 StringPart::ArgCount => {
421 let value = self.eval_arg_count()?;
422 result.push_str(&value_to_string(&value));
423 }
424 StringPart::Arithmetic(expr) => {
425 let value = self.eval_arithmetic_string(expr)?;
427 result.push_str(&value_to_string(&value));
428 }
429 StringPart::CommandSubst(stmts) => {
430 let value = self.eval_command_subst(stmts)?;
432 result.push_str(&value_to_string(&value));
433 }
434 StringPart::LastExitCode => {
435 result.push_str(&self.scope.last_result().code.to_string());
436 }
437 StringPart::CurrentPid => {
438 result.push_str(&self.scope.pid().to_string());
439 }
440 }
441 }
442 Ok(Value::String(result))
443 }
444
445 fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
447 arithmetic::eval_arithmetic(expr, self.scope)
449 .map(Value::Int)
450 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
451 }
452
453 fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
457 match op {
458 BinaryOp::And => {
459 let left_val = self.eval(left)?;
460 if !is_truthy(&left_val) {
461 return Ok(left_val);
462 }
463 self.eval(right)
464 }
465 BinaryOp::Or => {
466 let left_val = self.eval(left)?;
467 if is_truthy(&left_val) {
468 return Ok(left_val);
469 }
470 self.eval(right)
471 }
472 }
473 }
474
475 fn eval_command_subst(&mut self, stmts: &[Stmt]) -> EvalResult<Value> {
477 let result = self.executor.execute(stmts, self.scope)?;
478
479 self.scope.set_last_result(result.clone());
481
482 Ok(result_to_value(&result))
485 }
486}
487
488pub fn value_to_exit_code(value: &Value) -> anyhow::Result<i64> {
495 match value {
496 Value::Int(n) => Ok(*n),
497 Value::Bool(b) => Ok(if *b { 0 } else { 1 }),
498 Value::Float(f) => Ok(*f as i64),
499 Value::String(s) => {
500 let trimmed = s.trim();
501 trimmed.parse::<i64>().map_err(|_| {
502 anyhow::anyhow!("numeric argument required: {:?}", s)
503 })
504 }
505 Value::Null | Value::Json(_) | Value::Bytes(_) => {
506 anyhow::bail!("numeric argument required (got {:?})", value)
507 }
508 }
509}
510
511pub fn value_to_string(value: &Value) -> String {
512 match value {
513 Value::Null => "null".to_string(),
514 Value::Bool(b) => b.to_string(),
515 Value::Int(i) => i.to_string(),
516 Value::Float(f) => f.to_string(),
517 Value::String(s) => s.clone(),
518 Value::Json(json) => json.to_string(),
519 Value::Bytes(b) => format!("[binary: {} bytes]", b.len()),
522 }
523}
524
525pub fn value_to_bool(value: &Value) -> bool {
535 match value {
536 Value::Null => false,
537 Value::Bool(b) => *b,
538 Value::Int(i) => *i != 0,
539 Value::Float(f) => *f != 0.0,
540 Value::String(s) => !s.is_empty(),
541 Value::Json(json) => match json {
542 serde_json::Value::Null => false,
543 serde_json::Value::Array(arr) => !arr.is_empty(),
544 serde_json::Value::Object(obj) => !obj.is_empty(),
545 serde_json::Value::Bool(b) => *b,
546 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
547 serde_json::Value::String(s) => !s.is_empty(),
548 },
549 Value::Bytes(b) => !b.is_empty(), }
551}
552
553pub fn expand_tilde(s: &str, home: Option<&str>) -> String {
567 if s == "~" {
568 home.map(|h| h.to_string()).unwrap_or_else(|| "~".to_string())
569 } else if s.starts_with("~/") {
570 match home {
571 Some(home) => format!("{}{}", home, &s[1..]),
572 None => s.to_string(),
573 }
574 } else if s.starts_with('~') {
575 expand_tilde_user(s)
577 } else {
578 s.to_string()
579 }
580}
581
582#[cfg(all(unix, feature = "host"))]
587fn expand_tilde_user(s: &str) -> String {
588 let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
590 (&s[1..slash_pos + 1], &s[slash_pos + 1..])
591 } else {
592 (&s[1..], "")
593 };
594
595 if username.is_empty() {
596 return s.to_string();
597 }
598
599 let passwd = match std::fs::read_to_string("/etc/passwd") {
602 Ok(content) => content,
603 Err(_) => return s.to_string(),
604 };
605
606 for line in passwd.lines() {
607 let fields: Vec<&str> = line.split(':').collect();
608 if fields.len() >= 6 && fields[0] == username {
609 let home_dir = fields[5];
610 return if rest.is_empty() {
611 home_dir.to_string()
612 } else {
613 format!("{}{}", home_dir, rest)
614 };
615 }
616 }
617
618 s.to_string()
620}
621
622#[cfg(not(all(unix, feature = "host")))]
623fn expand_tilde_user(s: &str) -> String {
624 s.to_string()
627}
628
629pub fn value_to_string_with_tilde(value: &Value, home: Option<&str>) -> String {
634 match value {
635 Value::String(s) if s.starts_with('~') => expand_tilde(s, home),
636 _ => value_to_string(value),
637 }
638}
639
640fn format_path(path: &VarPath) -> String {
642 use crate::ast::VarSegment;
643 let mut result = String::from("${");
644 for (i, seg) in path.segments.iter().enumerate() {
645 match seg {
646 VarSegment::Field(name) => {
647 if i > 0 {
648 result.push('.');
649 }
650 result.push_str(name);
651 }
652 }
653 }
654 result.push('}');
655 result
656}
657
658fn is_truthy(value: &Value) -> bool {
668 value_to_bool(value)
670}
671
672fn values_equal(left: &Value, right: &Value) -> bool {
683 match (left, right) {
684 (Value::Null, Value::Null) => true,
685 (Value::Bool(a), Value::Bool(b)) => a == b,
686 (Value::Int(a), Value::Int(b)) => a == b,
687 (Value::Float(a), Value::Float(b)) => (a - b).abs() < f64::EPSILON,
688 (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
689 (*a as f64 - b).abs() < f64::EPSILON
690 }
691 (Value::String(a), Value::String(b)) => a == b,
692 (Value::Json(a), Value::Json(b)) => a == b,
693 (Value::Bytes(a), Value::Bytes(b)) => a == b,
694 _ => value_to_string(left) == value_to_string(right),
697 }
698}
699
700fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
702 match (left, right) {
703 (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
704 (Value::Float(a), Value::Float(b)) => {
705 a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
706 }
707 (Value::Int(a), Value::Float(b)) => {
708 (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
709 }
710 (Value::Float(a), Value::Int(b)) => {
711 a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
712 }
713 (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
714 _ => Err(EvalError::TypeError {
715 expected: "comparable types (numbers or strings)",
716 got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
717 }),
718 }
719}
720
721enum Num {
726 Int(i64),
727 Float(f64),
728}
729
730fn value_to_num(value: &Value) -> EvalResult<Num> {
731 match value {
732 Value::Int(n) => Ok(Num::Int(*n)),
733 Value::Float(f) => Ok(Num::Float(*f)),
734 Value::String(s) => {
735 let t = s.trim();
736 if let Ok(n) = t.parse::<i64>() {
737 Ok(Num::Int(n))
738 } else if let Ok(f) = t.parse::<f64>() {
739 Ok(Num::Float(f))
740 } else {
741 Err(EvalError::TypeError {
742 expected: "numeric operand",
743 got: format!("non-numeric string {:?}", s),
744 })
745 }
746 }
747 _ => Err(EvalError::TypeError {
748 expected: "numeric operand",
749 got: type_name(value).to_string(),
750 }),
751 }
752}
753
754fn numeric_compare(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
757 let l = value_to_num(left)?;
758 let r = value_to_num(right)?;
759 match (l, r) {
760 (Num::Int(a), Num::Int(b)) => Ok(a.cmp(&b)),
761 (Num::Float(a), Num::Float(b)) => a
762 .partial_cmp(&b)
763 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
764 (Num::Int(a), Num::Float(b)) => (a as f64)
765 .partial_cmp(&b)
766 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
767 (Num::Float(a), Num::Int(b)) => a
768 .partial_cmp(&(b as f64))
769 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
770 }
771}
772
773fn type_name(value: &Value) -> &'static str {
775 match value {
776 Value::Null => "null",
777 Value::Bool(_) => "bool",
778 Value::Int(_) => "int",
779 Value::Float(_) => "float",
780 Value::String(_) => "string",
781 Value::Json(_) => "json",
782 Value::Bytes(_) => "bytes",
783 }
784}
785
786fn result_to_value(result: &ExecResult) -> Value {
793 if let Some(data) = &result.data {
795 return data.clone();
796 }
797 Value::String(result.text_out().trim_end_matches('\n').to_string())
804}
805
806fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
811 let text = match left {
812 Value::String(s) => s.as_str(),
813 _ => {
814 return Err(EvalError::TypeError {
815 expected: "string",
816 got: type_name(left).to_string(),
817 })
818 }
819 };
820
821 let pattern = match right {
822 Value::String(s) => s.as_str(),
823 _ => {
824 return Err(EvalError::TypeError {
825 expected: "string (regex pattern)",
826 got: type_name(right).to_string(),
827 })
828 }
829 };
830
831 let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
832 let matches = re.is_match(text);
833
834 Ok(Value::Bool(if negate { !matches } else { matches }))
835}
836
837pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
841 let mut executor = NoOpExecutor;
842 let mut evaluator = Evaluator::new(scope, &mut executor);
843 evaluator.eval(expr)
844}
845
846#[cfg(test)]
847#[allow(clippy::approx_constant)]
848mod tests {
849 use super::*;
850 use crate::ast::VarSegment;
851
852 fn var_expr(name: &str) -> Expr {
854 Expr::VarRef(VarPath::simple(name))
855 }
856
857 #[test]
858 fn eval_literal_int() {
859 let mut scope = Scope::new();
860 let expr = Expr::Literal(Value::Int(42));
861 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
862 }
863
864 #[test]
865 fn eval_literal_string() {
866 let mut scope = Scope::new();
867 let expr = Expr::Literal(Value::String("hello".into()));
868 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
869 }
870
871 #[test]
872 fn eval_literal_bool() {
873 let mut scope = Scope::new();
874 assert_eq!(
875 eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
876 Ok(Value::Bool(true))
877 );
878 }
879
880 #[test]
881 fn eval_literal_null() {
882 let mut scope = Scope::new();
883 assert_eq!(
884 eval_expr(&Expr::Literal(Value::Null), &mut scope),
885 Ok(Value::Null)
886 );
887 }
888
889 #[test]
890 fn eval_literal_float() {
891 let mut scope = Scope::new();
892 let expr = Expr::Literal(Value::Float(3.14));
893 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
894 }
895
896 #[test]
897 fn eval_variable_ref() {
898 let mut scope = Scope::new();
899 scope.set("X", Value::Int(100));
900 assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
901 }
902
903 #[test]
904 fn eval_undefined_variable() {
905 let mut scope = Scope::new();
906 let result = eval_expr(&var_expr("MISSING"), &mut scope);
907 assert!(matches!(result, Err(EvalError::InvalidPath(_))));
908 }
909
910 #[test]
911 fn eval_interpolated_string() {
912 let mut scope = Scope::new();
913 scope.set("NAME", Value::String("World".into()));
914
915 let expr = Expr::Interpolated(vec![
916 StringPart::Literal("Hello, ".into()),
917 StringPart::Var(VarPath::simple("NAME")),
918 StringPart::Literal("!".into()),
919 ]);
920 assert_eq!(
921 eval_expr(&expr, &mut scope),
922 Ok(Value::String("Hello, World!".into()))
923 );
924 }
925
926 #[test]
927 fn eval_interpolated_with_number() {
928 let mut scope = Scope::new();
929 scope.set("COUNT", Value::Int(42));
930
931 let expr = Expr::Interpolated(vec![
932 StringPart::Literal("Count: ".into()),
933 StringPart::Var(VarPath::simple("COUNT")),
934 ]);
935 assert_eq!(
936 eval_expr(&expr, &mut scope),
937 Ok(Value::String("Count: 42".into()))
938 );
939 }
940
941 #[test]
942 fn eval_and_short_circuit_true() {
943 let mut scope = Scope::new();
944 let expr = Expr::BinaryOp {
945 left: Box::new(Expr::Literal(Value::Bool(true))),
946 op: BinaryOp::And,
947 right: Box::new(Expr::Literal(Value::Int(42))),
948 };
949 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
951 }
952
953 #[test]
954 fn eval_and_short_circuit_false() {
955 let mut scope = Scope::new();
956 let expr = Expr::BinaryOp {
957 left: Box::new(Expr::Literal(Value::Bool(false))),
958 op: BinaryOp::And,
959 right: Box::new(Expr::Literal(Value::Int(42))),
960 };
961 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
963 }
964
965 #[test]
966 fn eval_or_short_circuit_true() {
967 let mut scope = Scope::new();
968 let expr = Expr::BinaryOp {
969 left: Box::new(Expr::Literal(Value::Bool(true))),
970 op: BinaryOp::Or,
971 right: Box::new(Expr::Literal(Value::Int(42))),
972 };
973 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
975 }
976
977 #[test]
978 fn eval_or_short_circuit_false() {
979 let mut scope = Scope::new();
980 let expr = Expr::BinaryOp {
981 left: Box::new(Expr::Literal(Value::Bool(false))),
982 op: BinaryOp::Or,
983 right: Box::new(Expr::Literal(Value::Int(42))),
984 };
985 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
987 }
988
989 #[test]
990 fn is_truthy_values() {
991 assert!(!is_truthy(&Value::Null));
992 assert!(!is_truthy(&Value::Bool(false)));
993 assert!(is_truthy(&Value::Bool(true)));
994 assert!(!is_truthy(&Value::Int(0)));
995 assert!(is_truthy(&Value::Int(1)));
996 assert!(is_truthy(&Value::Int(-1)));
997 assert!(!is_truthy(&Value::Float(0.0)));
998 assert!(is_truthy(&Value::Float(0.1)));
999 assert!(!is_truthy(&Value::String("".into())));
1000 assert!(is_truthy(&Value::String("x".into())));
1001 }
1002
1003 #[test]
1004 fn eval_command_subst_fails_without_executor() {
1005 use crate::ast::Command;
1006
1007 let mut scope = Scope::new();
1008 let expr = Expr::CommandSubst(vec![Stmt::Command(Command {
1009 name: "echo".into(),
1010 args: vec![],
1011 redirects: vec![],
1012 })]);
1013
1014 assert!(matches!(
1015 eval_expr(&expr, &mut scope),
1016 Err(EvalError::NoExecutor)
1017 ));
1018 }
1019
1020 #[test]
1021 fn eval_last_result_bare() {
1022 let mut scope = Scope::new();
1025 scope.set_last_result(ExecResult::failure(42, "test error"));
1026
1027 let expr = Expr::VarRef(VarPath {
1028 segments: vec![VarSegment::Field("?".into())],
1029 });
1030 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1031 }
1032
1033 #[test]
1034 fn value_to_string_all_types() {
1035 assert_eq!(value_to_string(&Value::Null), "null");
1036 assert_eq!(value_to_string(&Value::Bool(true)), "true");
1037 assert_eq!(value_to_string(&Value::Int(42)), "42");
1038 assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1039 assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1040 }
1041
1042 #[test]
1045 fn eval_negative_int() {
1046 let mut scope = Scope::new();
1047 let expr = Expr::Literal(Value::Int(-42));
1048 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1049 }
1050
1051 #[test]
1052 fn eval_negative_float() {
1053 let mut scope = Scope::new();
1054 let expr = Expr::Literal(Value::Float(-3.14));
1055 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1056 }
1057
1058 #[test]
1059 fn eval_zero_values() {
1060 let mut scope = Scope::new();
1061 assert_eq!(
1062 eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1063 Ok(Value::Int(0))
1064 );
1065 assert_eq!(
1066 eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1067 Ok(Value::Float(0.0))
1068 );
1069 }
1070
1071 #[test]
1072 fn eval_interpolation_empty_var() {
1073 let mut scope = Scope::new();
1074 scope.set("EMPTY", Value::String("".into()));
1075
1076 let expr = Expr::Interpolated(vec![
1077 StringPart::Literal("prefix".into()),
1078 StringPart::Var(VarPath::simple("EMPTY")),
1079 StringPart::Literal("suffix".into()),
1080 ]);
1081 assert_eq!(
1082 eval_expr(&expr, &mut scope),
1083 Ok(Value::String("prefixsuffix".into()))
1084 );
1085 }
1086
1087 #[test]
1088 fn eval_chained_and() {
1089 let mut scope = Scope::new();
1090 let expr = Expr::BinaryOp {
1092 left: Box::new(Expr::BinaryOp {
1093 left: Box::new(Expr::Literal(Value::Bool(true))),
1094 op: BinaryOp::And,
1095 right: Box::new(Expr::Literal(Value::Bool(true))),
1096 }),
1097 op: BinaryOp::And,
1098 right: Box::new(Expr::Literal(Value::Int(42))),
1099 };
1100 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1101 }
1102
1103 #[test]
1104 fn eval_chained_or() {
1105 let mut scope = Scope::new();
1106 let expr = Expr::BinaryOp {
1108 left: Box::new(Expr::BinaryOp {
1109 left: Box::new(Expr::Literal(Value::Bool(false))),
1110 op: BinaryOp::Or,
1111 right: Box::new(Expr::Literal(Value::Bool(false))),
1112 }),
1113 op: BinaryOp::Or,
1114 right: Box::new(Expr::Literal(Value::Int(42))),
1115 };
1116 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1117 }
1118
1119 #[test]
1120 fn eval_mixed_and_or() {
1121 let mut scope = Scope::new();
1122 let expr = Expr::BinaryOp {
1125 left: Box::new(Expr::BinaryOp {
1126 left: Box::new(Expr::Literal(Value::Bool(true))),
1127 op: BinaryOp::Or,
1128 right: Box::new(Expr::Literal(Value::Bool(false))),
1129 }),
1130 op: BinaryOp::And,
1131 right: Box::new(Expr::Literal(Value::Bool(true))),
1132 };
1133 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1135 }
1136
1137 #[test]
1138 fn eval_interpolation_with_bool() {
1139 let mut scope = Scope::new();
1140 scope.set("FLAG", Value::Bool(true));
1141
1142 let expr = Expr::Interpolated(vec![
1143 StringPart::Literal("enabled: ".into()),
1144 StringPart::Var(VarPath::simple("FLAG")),
1145 ]);
1146 assert_eq!(
1147 eval_expr(&expr, &mut scope),
1148 Ok(Value::String("enabled: true".into()))
1149 );
1150 }
1151
1152 #[test]
1153 fn eval_interpolation_with_null() {
1154 let mut scope = Scope::new();
1155 scope.set("VAL", Value::Null);
1156
1157 let expr = Expr::Interpolated(vec![
1158 StringPart::Literal("value: ".into()),
1159 StringPart::Var(VarPath::simple("VAL")),
1160 ]);
1161 assert_eq!(
1162 eval_expr(&expr, &mut scope),
1163 Ok(Value::String("value: null".into()))
1164 );
1165 }
1166
1167 #[test]
1168 fn eval_format_path_simple() {
1169 let path = VarPath::simple("X");
1170 assert_eq!(format_path(&path), "${X}");
1171 }
1172
1173 #[test]
1174 fn eval_format_path_nested() {
1175 let path = VarPath {
1176 segments: vec![
1177 VarSegment::Field("X".into()),
1178 VarSegment::Field("field".into()),
1179 ],
1180 };
1181 assert_eq!(format_path(&path), "${X.field}");
1182 }
1183
1184 #[test]
1185 fn type_name_all_types() {
1186 assert_eq!(type_name(&Value::Null), "null");
1187 assert_eq!(type_name(&Value::Bool(true)), "bool");
1188 assert_eq!(type_name(&Value::Int(1)), "int");
1189 assert_eq!(type_name(&Value::Float(1.0)), "float");
1190 assert_eq!(type_name(&Value::String("".into())), "string");
1191 }
1192
1193 #[test]
1194 fn expand_tilde_home() {
1195 let home = "/home/session";
1197 assert_eq!(expand_tilde("~", Some(home)), home);
1198 assert_eq!(expand_tilde("~/foo", Some(home)), format!("{}/foo", home));
1199 assert_eq!(
1200 expand_tilde("~/foo/bar", Some(home)),
1201 format!("{}/foo/bar", home)
1202 );
1203 }
1204
1205 #[test]
1206 fn expand_tilde_hermetic_no_home_does_not_leak_host() {
1207 assert_eq!(expand_tilde("~", None), "~");
1210 assert_eq!(expand_tilde("~/foo", None), "~/foo");
1211 }
1212
1213 #[test]
1214 fn expand_tilde_passthrough() {
1215 assert_eq!(expand_tilde("/home/user", Some("/h")), "/home/user");
1217 assert_eq!(expand_tilde("foo~bar", Some("/h")), "foo~bar");
1218 assert_eq!(expand_tilde("", Some("/h")), "");
1219 }
1220
1221 #[test]
1222 #[cfg(all(unix, feature = "host"))]
1223 fn expand_tilde_user() {
1224 let expanded = expand_tilde("~root", None);
1227 assert!(
1229 expanded == "/root" || expanded == "/var/root",
1230 "expected /root or /var/root, got: {}",
1231 expanded
1232 );
1233
1234 let expanded_path = expand_tilde("~root/subdir", None);
1236 assert!(
1237 expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1238 "expected /root/subdir or /var/root/subdir, got: {}",
1239 expanded_path
1240 );
1241
1242 let nonexistent = expand_tilde("~nonexistent_user_12345", None);
1244 assert_eq!(nonexistent, "~nonexistent_user_12345");
1245 }
1246
1247 #[test]
1248 fn value_to_string_with_tilde_expansion() {
1249 let val = Value::String("~/test".into());
1251 assert_eq!(
1252 value_to_string_with_tilde(&val, Some("/home/session")),
1253 "/home/session/test"
1254 );
1255 }
1256
1257 #[test]
1258 fn eval_positional_param() {
1259 let mut scope = Scope::new();
1260 scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1261
1262 let expr = Expr::Positional(0);
1264 let result = eval_expr(&expr, &mut scope).unwrap();
1265 assert_eq!(result, Value::String("my_tool".into()));
1266
1267 let expr = Expr::Positional(1);
1269 let result = eval_expr(&expr, &mut scope).unwrap();
1270 assert_eq!(result, Value::String("hello".into()));
1271
1272 let expr = Expr::Positional(2);
1274 let result = eval_expr(&expr, &mut scope).unwrap();
1275 assert_eq!(result, Value::String("world".into()));
1276
1277 let expr = Expr::Positional(3);
1279 let result = eval_expr(&expr, &mut scope).unwrap();
1280 assert_eq!(result, Value::String("".into()));
1281 }
1282
1283 #[test]
1284 fn eval_all_args() {
1285 let mut scope = Scope::new();
1286 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1287
1288 let expr = Expr::AllArgs;
1289 let result = eval_expr(&expr, &mut scope).unwrap();
1290
1291 assert_eq!(result, Value::String("a b c".into()));
1293 }
1294
1295 #[test]
1296 fn eval_arg_count() {
1297 let mut scope = Scope::new();
1298 scope.set_positional("test", vec!["x".into(), "y".into()]);
1299
1300 let expr = Expr::ArgCount;
1301 let result = eval_expr(&expr, &mut scope).unwrap();
1302 assert_eq!(result, Value::Int(2));
1303 }
1304
1305 #[test]
1306 fn eval_arg_count_empty() {
1307 let mut scope = Scope::new();
1308
1309 let expr = Expr::ArgCount;
1310 let result = eval_expr(&expr, &mut scope).unwrap();
1311 assert_eq!(result, Value::Int(0));
1312 }
1313
1314 #[test]
1315 fn eval_var_length_string() {
1316 let mut scope = Scope::new();
1317 scope.set("NAME", Value::String("hello".into()));
1318
1319 let expr = Expr::VarLength("NAME".into());
1320 let result = eval_expr(&expr, &mut scope).unwrap();
1321 assert_eq!(result, Value::Int(5));
1322 }
1323
1324 #[test]
1325 fn eval_var_length_empty_string() {
1326 let mut scope = Scope::new();
1327 scope.set("EMPTY", Value::String("".into()));
1328
1329 let expr = Expr::VarLength("EMPTY".into());
1330 let result = eval_expr(&expr, &mut scope).unwrap();
1331 assert_eq!(result, Value::Int(0));
1332 }
1333
1334 #[test]
1335 fn eval_var_length_unset() {
1336 let mut scope = Scope::new();
1337
1338 let expr = Expr::VarLength("MISSING".into());
1340 let result = eval_expr(&expr, &mut scope).unwrap();
1341 assert_eq!(result, Value::Int(0));
1342 }
1343
1344 #[test]
1345 fn eval_var_length_int() {
1346 let mut scope = Scope::new();
1347 scope.set("NUM", Value::Int(12345));
1348
1349 let expr = Expr::VarLength("NUM".into());
1351 let result = eval_expr(&expr, &mut scope).unwrap();
1352 assert_eq!(result, Value::Int(5)); }
1354
1355 #[test]
1356 fn eval_var_with_default_set() {
1357 let mut scope = Scope::new();
1358 scope.set("NAME", Value::String("Alice".into()));
1359
1360 let expr = Expr::VarWithDefault {
1362 name: "NAME".into(),
1363 default: vec![StringPart::Literal("default".into())],
1364 };
1365 let result = eval_expr(&expr, &mut scope).unwrap();
1366 assert_eq!(result, Value::String("Alice".into()));
1367 }
1368
1369 #[test]
1370 fn eval_var_with_default_unset() {
1371 let mut scope = Scope::new();
1372
1373 let expr = Expr::VarWithDefault {
1375 name: "MISSING".into(),
1376 default: vec![StringPart::Literal("fallback".into())],
1377 };
1378 let result = eval_expr(&expr, &mut scope).unwrap();
1379 assert_eq!(result, Value::String("fallback".into()));
1380 }
1381
1382 #[test]
1383 fn eval_var_with_default_empty() {
1384 let mut scope = Scope::new();
1385 scope.set("EMPTY", Value::String("".into()));
1386
1387 let expr = Expr::VarWithDefault {
1389 name: "EMPTY".into(),
1390 default: vec![StringPart::Literal("not empty".into())],
1391 };
1392 let result = eval_expr(&expr, &mut scope).unwrap();
1393 assert_eq!(result, Value::String("not empty".into()));
1394 }
1395
1396 #[test]
1397 fn eval_var_with_default_non_string() {
1398 let mut scope = Scope::new();
1399 scope.set("NUM", Value::Int(42));
1400
1401 let expr = Expr::VarWithDefault {
1403 name: "NUM".into(),
1404 default: vec![StringPart::Literal("default".into())],
1405 };
1406 let result = eval_expr(&expr, &mut scope).unwrap();
1407 assert_eq!(result, Value::Int(42));
1408 }
1409
1410 #[test]
1411 fn eval_unset_variable_is_empty() {
1412 let mut scope = Scope::new();
1413 let parts = vec![
1414 StringPart::Literal("prefix:".into()),
1415 StringPart::Var(VarPath::simple("UNSET")),
1416 StringPart::Literal(":suffix".into()),
1417 ];
1418 let expr = Expr::Interpolated(parts);
1419 let result = eval_expr(&expr, &mut scope).unwrap();
1420 assert_eq!(result, Value::String("prefix::suffix".into()));
1421 }
1422
1423 #[test]
1424 fn eval_unset_variable_multiple() {
1425 let mut scope = Scope::new();
1426 scope.set("SET", Value::String("hello".into()));
1427 let parts = vec![
1428 StringPart::Var(VarPath::simple("UNSET1")),
1429 StringPart::Literal("-".into()),
1430 StringPart::Var(VarPath::simple("SET")),
1431 StringPart::Literal("-".into()),
1432 StringPart::Var(VarPath::simple("UNSET2")),
1433 ];
1434 let expr = Expr::Interpolated(parts);
1435 let result = eval_expr(&expr, &mut scope).unwrap();
1436 assert_eq!(result, Value::String("-hello-".into()));
1437 }
1438}