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