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
20#[derive(Debug, Clone, PartialEq)]
22pub enum EvalError {
23 UndefinedVariable(String),
25 InvalidPath(String),
27 TypeError { expected: &'static str, got: String },
29 CommandFailed(String),
31 NoExecutor,
33 ArithmeticError(String),
35 RegexError(String),
37}
38
39impl fmt::Display for EvalError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 EvalError::UndefinedVariable(name) => write!(f, "undefined variable: {name}"),
43 EvalError::InvalidPath(path) => write!(f, "invalid path: {path}"),
44 EvalError::TypeError { expected, got } => {
45 write!(f, "type error: expected {expected}, got {got}")
46 }
47 EvalError::CommandFailed(msg) => write!(f, "command failed: {msg}"),
48 EvalError::NoExecutor => write!(f, "no executor available for command substitution"),
49 EvalError::ArithmeticError(msg) => write!(f, "arithmetic error: {msg}"),
50 EvalError::RegexError(msg) => write!(f, "regex error: {msg}"),
51 }
52 }
53}
54
55impl std::error::Error for EvalError {}
56
57pub type EvalResult<T> = Result<T, EvalError>;
59
60pub trait Executor {
66 fn execute(&mut self, pipeline: &Pipeline, scope: &mut Scope) -> EvalResult<ExecResult>;
73
74 fn file_stat(&self, path: &Path) -> Option<DirEntry> {
81 std::fs::metadata(path).ok().map(|meta| {
82 if meta.is_dir() {
83 DirEntry::directory(path.file_name().unwrap_or_default().to_string_lossy())
84 } else {
85 let mut entry = DirEntry::file(
86 path.file_name().unwrap_or_default().to_string_lossy(),
87 meta.len(),
88 );
89 #[cfg(unix)]
90 {
91 use std::os::unix::fs::PermissionsExt;
92 entry.permissions = Some(meta.permissions().mode());
93 }
94 entry
95 }
96 })
97 }
98}
99
100pub struct NoOpExecutor;
104
105impl Executor for NoOpExecutor {
106 fn execute(&mut self, _pipeline: &Pipeline, _scope: &mut Scope) -> EvalResult<ExecResult> {
107 Err(EvalError::NoExecutor)
108 }
109}
110
111pub struct Evaluator<'a, E: Executor> {
116 scope: &'a mut Scope,
117 executor: &'a mut E,
118}
119
120impl<'a, E: Executor> Evaluator<'a, E> {
121 pub fn new(scope: &'a mut Scope, executor: &'a mut E) -> Self {
123 Self { scope, executor }
124 }
125
126 pub fn eval(&mut self, expr: &Expr) -> EvalResult<Value> {
128 match expr {
129 Expr::Literal(value) => self.eval_literal(value),
130 Expr::VarRef(path) => self.eval_var_ref(path),
131 Expr::Interpolated(parts) => self.eval_interpolated(parts),
132 Expr::BinaryOp { left, op, right } => self.eval_binary_op(left, *op, right),
133 Expr::CommandSubst(pipeline) => self.eval_command_subst(pipeline),
134 Expr::Test(test_expr) => self.eval_test(test_expr),
135 Expr::Positional(n) => self.eval_positional(*n),
136 Expr::AllArgs => self.eval_all_args(),
137 Expr::ArgCount => self.eval_arg_count(),
138 Expr::VarLength(name) => self.eval_var_length(name),
139 Expr::VarWithDefault { name, default } => self.eval_var_with_default(name, default),
140 Expr::Arithmetic(expr_str) => self.eval_arithmetic(expr_str),
141 Expr::Command(cmd) => self.eval_command(cmd),
142 Expr::LastExitCode => self.eval_last_exit_code(),
143 Expr::CurrentPid => self.eval_current_pid(),
144 }
145 }
146
147 fn eval_last_exit_code(&self) -> EvalResult<Value> {
149 Ok(Value::Int(self.scope.last_result().code))
150 }
151
152 fn eval_current_pid(&self) -> EvalResult<Value> {
154 Ok(Value::Int(self.scope.pid() as i64))
155 }
156
157 fn eval_command(&mut self, cmd: &crate::ast::Command) -> EvalResult<Value> {
159 match cmd.name.as_str() {
162 "true" => return Ok(Value::Bool(true)),
163 "false" => return Ok(Value::Bool(false)),
164 _ => {}
165 }
166
167 let pipeline = crate::ast::Pipeline {
169 commands: vec![cmd.clone()],
170 background: false,
171 };
172 let result = self.executor.execute(&pipeline, self.scope)?;
173 Ok(Value::Bool(result.code == 0))
175 }
176
177 fn eval_arithmetic(&mut self, expr_str: &str) -> EvalResult<Value> {
179 arithmetic::eval_arithmetic(expr_str, self.scope)
180 .map(Value::Int)
181 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
182 }
183
184 fn eval_test(&mut self, test_expr: &TestExpr) -> EvalResult<Value> {
186 let result = match test_expr {
187 TestExpr::FileTest { op, path } => {
188 let path_value = self.eval(path)?;
189 let path_str = value_to_string(&path_value);
190 let path = Path::new(&path_str);
191 let entry = self.executor.file_stat(path);
192 match op {
193 FileTestOp::Exists => entry.is_some(),
194 FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
195 FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
196 FileTestOp::Readable => entry.is_some(),
197 FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
198 e.permissions.is_none_or(|p| p & 0o222 != 0)
199 }),
200 FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
201 e.permissions.is_some_and(|p| p & 0o111 != 0)
202 }),
203 }
204 }
205 TestExpr::StringTest { op, value } => {
206 let val = self.eval(value)?;
207 let s = value_to_string(&val);
208 match op {
209 StringTestOp::IsEmpty => s.is_empty(),
210 StringTestOp::IsNonEmpty => !s.is_empty(),
211 }
212 }
213 TestExpr::Comparison { left, op, right } => {
214 let left_val = self.eval(left)?;
215 let right_val = self.eval(right)?;
216
217 match op {
218 TestCmpOp::Eq => values_equal(&left_val, &right_val),
219 TestCmpOp::NotEq => !values_equal(&left_val, &right_val),
220 TestCmpOp::Match => {
221 match regex_match(&left_val, &right_val, false) {
223 Ok(Value::Bool(b)) => b,
224 Ok(_) => false,
225 Err(_) => false,
226 }
227 }
228 TestCmpOp::NotMatch => {
229 match regex_match(&left_val, &right_val, true) {
231 Ok(Value::Bool(b)) => b,
232 Ok(_) => true,
233 Err(_) => true,
234 }
235 }
236 TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
237 let ord = compare_values(&left_val, &right_val)?;
240 match op {
241 TestCmpOp::Gt => ord.is_gt(),
242 TestCmpOp::Lt => ord.is_lt(),
243 TestCmpOp::GtEq => ord.is_ge(),
244 TestCmpOp::LtEq => ord.is_le(),
245 _ => unreachable!(),
246 }
247 }
248 }
249 }
250 TestExpr::And { left, right } => {
251 let left_result = self.eval_test(left)?;
253 if !value_to_bool(&left_result) {
254 false } else {
256 value_to_bool(&self.eval_test(right)?)
257 }
258 }
259 TestExpr::Or { left, right } => {
260 let left_result = self.eval_test(left)?;
262 if value_to_bool(&left_result) {
263 true } else {
265 value_to_bool(&self.eval_test(right)?)
266 }
267 }
268 TestExpr::Not { expr } => {
269 let result = self.eval_test(expr)?;
270 !value_to_bool(&result)
271 }
272 };
273 Ok(Value::Bool(result))
274 }
275
276 fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
278 Ok(value.clone())
279 }
280
281 fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
283 self.scope
284 .resolve_path(path)
285 .ok_or_else(|| EvalError::InvalidPath(format_path(path)))
286 }
287
288 fn eval_positional(&self, n: usize) -> EvalResult<Value> {
290 match self.scope.get_positional(n) {
291 Some(s) => Ok(Value::String(s.to_string())),
292 None => Ok(Value::String(String::new())), }
294 }
295
296 fn eval_all_args(&self) -> EvalResult<Value> {
300 let args = self.scope.all_args();
301 Ok(Value::String(args.join(" ")))
302 }
303
304 fn eval_arg_count(&self) -> EvalResult<Value> {
306 Ok(Value::Int(self.scope.arg_count() as i64))
307 }
308
309 fn eval_var_length(&self, name: &str) -> EvalResult<Value> {
311 match self.scope.get(name) {
312 Some(value) => {
313 let s = value_to_string(value);
314 Ok(Value::Int(s.len() as i64))
315 }
316 None => Ok(Value::Int(0)), }
318 }
319
320 fn eval_var_with_default(&mut self, name: &str, default: &[StringPart]) -> EvalResult<Value> {
323 match self.scope.get(name) {
324 Some(value) => {
325 let s = value_to_string(value);
326 if s.is_empty() {
327 self.eval_interpolated(default)
329 } else {
330 Ok(value.clone())
331 }
332 }
333 None => {
334 self.eval_interpolated(default)
336 }
337 }
338 }
339
340 fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
342 let mut result = String::new();
343 for part in parts {
344 match part {
345 StringPart::Literal(s) => result.push_str(s),
346 StringPart::Var(path) => {
347 if let Some(value) = self.scope.resolve_path(path) {
349 result.push_str(&value_to_string(&value));
350 }
351 }
352 StringPart::VarWithDefault { name, default } => {
353 let value = self.eval_var_with_default(name, default)?;
354 result.push_str(&value_to_string(&value));
355 }
356 StringPart::VarLength(name) => {
357 let value = self.eval_var_length(name)?;
358 result.push_str(&value_to_string(&value));
359 }
360 StringPart::Positional(n) => {
361 let value = self.eval_positional(*n)?;
362 result.push_str(&value_to_string(&value));
363 }
364 StringPart::AllArgs => {
365 let value = self.eval_all_args()?;
366 result.push_str(&value_to_string(&value));
367 }
368 StringPart::ArgCount => {
369 let value = self.eval_arg_count()?;
370 result.push_str(&value_to_string(&value));
371 }
372 StringPart::Arithmetic(expr) => {
373 let value = self.eval_arithmetic_string(expr)?;
375 result.push_str(&value_to_string(&value));
376 }
377 StringPart::CommandSubst(pipeline) => {
378 let value = self.eval_command_subst(pipeline)?;
380 result.push_str(&value_to_string(&value));
381 }
382 StringPart::LastExitCode => {
383 result.push_str(&self.scope.last_result().code.to_string());
384 }
385 StringPart::CurrentPid => {
386 result.push_str(&self.scope.pid().to_string());
387 }
388 }
389 }
390 Ok(Value::String(result))
391 }
392
393 fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
395 arithmetic::eval_arithmetic(expr, self.scope)
397 .map(Value::Int)
398 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
399 }
400
401 fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
403 match op {
404 BinaryOp::And => {
406 let left_val = self.eval(left)?;
407 if !is_truthy(&left_val) {
408 return Ok(left_val);
409 }
410 self.eval(right)
411 }
412 BinaryOp::Or => {
413 let left_val = self.eval(left)?;
414 if is_truthy(&left_val) {
415 return Ok(left_val);
416 }
417 self.eval(right)
418 }
419 BinaryOp::Eq => {
421 let left_val = self.eval(left)?;
422 let right_val = self.eval(right)?;
423 Ok(Value::Bool(values_equal(&left_val, &right_val)))
424 }
425 BinaryOp::NotEq => {
426 let left_val = self.eval(left)?;
427 let right_val = self.eval(right)?;
428 Ok(Value::Bool(!values_equal(&left_val, &right_val)))
429 }
430 BinaryOp::Lt => {
431 let left_val = self.eval(left)?;
432 let right_val = self.eval(right)?;
433 compare_values(&left_val, &right_val).map(|ord| Value::Bool(ord.is_lt()))
434 }
435 BinaryOp::Gt => {
436 let left_val = self.eval(left)?;
437 let right_val = self.eval(right)?;
438 compare_values(&left_val, &right_val).map(|ord| Value::Bool(ord.is_gt()))
439 }
440 BinaryOp::LtEq => {
441 let left_val = self.eval(left)?;
442 let right_val = self.eval(right)?;
443 compare_values(&left_val, &right_val).map(|ord| Value::Bool(ord.is_le()))
444 }
445 BinaryOp::GtEq => {
446 let left_val = self.eval(left)?;
447 let right_val = self.eval(right)?;
448 compare_values(&left_val, &right_val).map(|ord| Value::Bool(ord.is_ge()))
449 }
450 BinaryOp::Match => {
452 let left_val = self.eval(left)?;
453 let right_val = self.eval(right)?;
454 regex_match(&left_val, &right_val, false)
455 }
456 BinaryOp::NotMatch => {
457 let left_val = self.eval(left)?;
458 let right_val = self.eval(right)?;
459 regex_match(&left_val, &right_val, true)
460 }
461 }
462 }
463
464 fn eval_command_subst(&mut self, pipeline: &Pipeline) -> EvalResult<Value> {
466 let result = self.executor.execute(pipeline, self.scope)?;
467
468 self.scope.set_last_result(result.clone());
470
471 Ok(result_to_value(&result))
474 }
475}
476
477pub fn value_to_string(value: &Value) -> String {
479 match value {
480 Value::Null => "null".to_string(),
481 Value::Bool(b) => b.to_string(),
482 Value::Int(i) => i.to_string(),
483 Value::Float(f) => f.to_string(),
484 Value::String(s) => s.clone(),
485 Value::Json(json) => json.to_string(),
486 Value::Blob(blob) => format!("[blob: {} {}]", blob.formatted_size(), blob.content_type),
487 }
488}
489
490pub fn value_to_bool(value: &Value) -> bool {
500 match value {
501 Value::Null => false,
502 Value::Bool(b) => *b,
503 Value::Int(i) => *i != 0,
504 Value::Float(f) => *f != 0.0,
505 Value::String(s) => !s.is_empty(),
506 Value::Json(json) => match json {
507 serde_json::Value::Null => false,
508 serde_json::Value::Array(arr) => !arr.is_empty(),
509 serde_json::Value::Object(obj) => !obj.is_empty(),
510 serde_json::Value::Bool(b) => *b,
511 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
512 serde_json::Value::String(s) => !s.is_empty(),
513 },
514 Value::Blob(_) => true, }
516}
517
518pub fn expand_tilde(s: &str) -> String {
526 if s == "~" {
527 std::env::var("HOME").unwrap_or_else(|_| "~".to_string())
528 } else if s.starts_with("~/") {
529 match std::env::var("HOME") {
530 Ok(home) => format!("{}{}", home, &s[1..]),
531 Err(_) => s.to_string(),
532 }
533 } else if s.starts_with('~') {
534 expand_tilde_user(s)
536 } else {
537 s.to_string()
538 }
539}
540
541#[cfg(unix)]
543fn expand_tilde_user(s: &str) -> String {
544 let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
546 (&s[1..slash_pos + 1], &s[slash_pos + 1..])
547 } else {
548 (&s[1..], "")
549 };
550
551 if username.is_empty() {
552 return s.to_string();
553 }
554
555 let passwd = match std::fs::read_to_string("/etc/passwd") {
558 Ok(content) => content,
559 Err(_) => return s.to_string(),
560 };
561
562 for line in passwd.lines() {
563 let fields: Vec<&str> = line.split(':').collect();
564 if fields.len() >= 6 && fields[0] == username {
565 let home_dir = fields[5];
566 return if rest.is_empty() {
567 home_dir.to_string()
568 } else {
569 format!("{}{}", home_dir, rest)
570 };
571 }
572 }
573
574 s.to_string()
576}
577
578#[cfg(not(unix))]
579fn expand_tilde_user(s: &str) -> String {
580 s.to_string()
582}
583
584pub fn value_to_string_with_tilde(value: &Value) -> String {
586 match value {
587 Value::String(s) if s.starts_with('~') => expand_tilde(s),
588 _ => value_to_string(value),
589 }
590}
591
592fn format_path(path: &VarPath) -> String {
594 use crate::ast::VarSegment;
595 let mut result = String::from("${");
596 for (i, seg) in path.segments.iter().enumerate() {
597 match seg {
598 VarSegment::Field(name) => {
599 if i > 0 {
600 result.push('.');
601 }
602 result.push_str(name);
603 }
604 }
605 }
606 result.push('}');
607 result
608}
609
610fn is_truthy(value: &Value) -> bool {
620 value_to_bool(value)
622}
623
624fn values_equal(left: &Value, right: &Value) -> bool {
632 match (left, right) {
633 (Value::Null, Value::Null) => true,
634 (Value::Bool(a), Value::Bool(b)) => a == b,
635 (Value::Int(a), Value::Int(b)) => a == b,
636 (Value::Float(a), Value::Float(b)) => (a - b).abs() < f64::EPSILON,
637 (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
638 (*a as f64 - b).abs() < f64::EPSILON
639 }
640 (Value::String(a), Value::String(b)) => a == b,
641 (Value::String(s), Value::Int(n)) | (Value::Int(n), Value::String(s)) => {
643 s.parse::<i64>().map(|parsed| parsed == *n).unwrap_or(false)
644 }
645 (Value::String(s), Value::Float(f)) | (Value::Float(f), Value::String(s)) => {
647 s.parse::<f64>().map(|parsed| (parsed - f).abs() < f64::EPSILON).unwrap_or(false)
648 }
649 (Value::Json(a), Value::Json(b)) => a == b,
651 (Value::Blob(a), Value::Blob(b)) => a.id == b.id,
653 _ => false,
654 }
655}
656
657fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
659 match (left, right) {
660 (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
661 (Value::Float(a), Value::Float(b)) => {
662 a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
663 }
664 (Value::Int(a), Value::Float(b)) => {
665 (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
666 }
667 (Value::Float(a), Value::Int(b)) => {
668 a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
669 }
670 (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
671 _ => Err(EvalError::TypeError {
672 expected: "comparable types (numbers or strings)",
673 got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
674 }),
675 }
676}
677
678fn type_name(value: &Value) -> &'static str {
680 match value {
681 Value::Null => "null",
682 Value::Bool(_) => "bool",
683 Value::Int(_) => "int",
684 Value::Float(_) => "float",
685 Value::String(_) => "string",
686 Value::Json(_) => "json",
687 Value::Blob(_) => "blob",
688 }
689}
690
691fn result_to_value(result: &ExecResult) -> Value {
697 if let Some(data) = &result.data {
699 return data.clone();
700 }
701 Value::String(result.out.trim_end().to_string())
703}
704
705fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
710 let text = match left {
711 Value::String(s) => s.as_str(),
712 _ => {
713 return Err(EvalError::TypeError {
714 expected: "string",
715 got: type_name(left).to_string(),
716 })
717 }
718 };
719
720 let pattern = match right {
721 Value::String(s) => s.as_str(),
722 _ => {
723 return Err(EvalError::TypeError {
724 expected: "string (regex pattern)",
725 got: type_name(right).to_string(),
726 })
727 }
728 };
729
730 let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
731 let matches = re.is_match(text);
732
733 Ok(Value::Bool(if negate { !matches } else { matches }))
734}
735
736pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
740 let mut executor = NoOpExecutor;
741 let mut evaluator = Evaluator::new(scope, &mut executor);
742 evaluator.eval(expr)
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748 use crate::ast::VarSegment;
749
750 fn var_expr(name: &str) -> Expr {
752 Expr::VarRef(VarPath::simple(name))
753 }
754
755 #[test]
756 fn eval_literal_int() {
757 let mut scope = Scope::new();
758 let expr = Expr::Literal(Value::Int(42));
759 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
760 }
761
762 #[test]
763 fn eval_literal_string() {
764 let mut scope = Scope::new();
765 let expr = Expr::Literal(Value::String("hello".into()));
766 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
767 }
768
769 #[test]
770 fn eval_literal_bool() {
771 let mut scope = Scope::new();
772 assert_eq!(
773 eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
774 Ok(Value::Bool(true))
775 );
776 }
777
778 #[test]
779 fn eval_literal_null() {
780 let mut scope = Scope::new();
781 assert_eq!(
782 eval_expr(&Expr::Literal(Value::Null), &mut scope),
783 Ok(Value::Null)
784 );
785 }
786
787 #[test]
788 fn eval_literal_float() {
789 let mut scope = Scope::new();
790 let expr = Expr::Literal(Value::Float(3.14));
791 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
792 }
793
794 #[test]
795 fn eval_variable_ref() {
796 let mut scope = Scope::new();
797 scope.set("X", Value::Int(100));
798 assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
799 }
800
801 #[test]
802 fn eval_undefined_variable() {
803 let mut scope = Scope::new();
804 let result = eval_expr(&var_expr("MISSING"), &mut scope);
805 assert!(matches!(result, Err(EvalError::InvalidPath(_))));
806 }
807
808 #[test]
809 fn eval_interpolated_string() {
810 let mut scope = Scope::new();
811 scope.set("NAME", Value::String("World".into()));
812
813 let expr = Expr::Interpolated(vec![
814 StringPart::Literal("Hello, ".into()),
815 StringPart::Var(VarPath::simple("NAME")),
816 StringPart::Literal("!".into()),
817 ]);
818 assert_eq!(
819 eval_expr(&expr, &mut scope),
820 Ok(Value::String("Hello, World!".into()))
821 );
822 }
823
824 #[test]
825 fn eval_interpolated_with_number() {
826 let mut scope = Scope::new();
827 scope.set("COUNT", Value::Int(42));
828
829 let expr = Expr::Interpolated(vec![
830 StringPart::Literal("Count: ".into()),
831 StringPart::Var(VarPath::simple("COUNT")),
832 ]);
833 assert_eq!(
834 eval_expr(&expr, &mut scope),
835 Ok(Value::String("Count: 42".into()))
836 );
837 }
838
839 #[test]
840 fn eval_and_short_circuit_true() {
841 let mut scope = Scope::new();
842 let expr = Expr::BinaryOp {
843 left: Box::new(Expr::Literal(Value::Bool(true))),
844 op: BinaryOp::And,
845 right: Box::new(Expr::Literal(Value::Int(42))),
846 };
847 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
849 }
850
851 #[test]
852 fn eval_and_short_circuit_false() {
853 let mut scope = Scope::new();
854 let expr = Expr::BinaryOp {
855 left: Box::new(Expr::Literal(Value::Bool(false))),
856 op: BinaryOp::And,
857 right: Box::new(Expr::Literal(Value::Int(42))),
858 };
859 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
861 }
862
863 #[test]
864 fn eval_or_short_circuit_true() {
865 let mut scope = Scope::new();
866 let expr = Expr::BinaryOp {
867 left: Box::new(Expr::Literal(Value::Bool(true))),
868 op: BinaryOp::Or,
869 right: Box::new(Expr::Literal(Value::Int(42))),
870 };
871 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
873 }
874
875 #[test]
876 fn eval_or_short_circuit_false() {
877 let mut scope = Scope::new();
878 let expr = Expr::BinaryOp {
879 left: Box::new(Expr::Literal(Value::Bool(false))),
880 op: BinaryOp::Or,
881 right: Box::new(Expr::Literal(Value::Int(42))),
882 };
883 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
885 }
886
887 #[test]
888 fn eval_equality() {
889 let mut scope = Scope::new();
890 let expr = Expr::BinaryOp {
891 left: Box::new(Expr::Literal(Value::Int(5))),
892 op: BinaryOp::Eq,
893 right: Box::new(Expr::Literal(Value::Int(5))),
894 };
895 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
896 }
897
898 #[test]
899 fn eval_inequality() {
900 let mut scope = Scope::new();
901 let expr = Expr::BinaryOp {
902 left: Box::new(Expr::Literal(Value::Int(5))),
903 op: BinaryOp::NotEq,
904 right: Box::new(Expr::Literal(Value::Int(3))),
905 };
906 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
907 }
908
909 #[test]
910 fn eval_less_than() {
911 let mut scope = Scope::new();
912 let expr = Expr::BinaryOp {
913 left: Box::new(Expr::Literal(Value::Int(3))),
914 op: BinaryOp::Lt,
915 right: Box::new(Expr::Literal(Value::Int(5))),
916 };
917 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
918 }
919
920 #[test]
921 fn eval_greater_than() {
922 let mut scope = Scope::new();
923 let expr = Expr::BinaryOp {
924 left: Box::new(Expr::Literal(Value::Int(5))),
925 op: BinaryOp::Gt,
926 right: Box::new(Expr::Literal(Value::Int(3))),
927 };
928 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
929 }
930
931 #[test]
932 fn eval_less_than_or_equal() {
933 let mut scope = Scope::new();
934 let eq = Expr::BinaryOp {
935 left: Box::new(Expr::Literal(Value::Int(5))),
936 op: BinaryOp::LtEq,
937 right: Box::new(Expr::Literal(Value::Int(5))),
938 };
939 let lt = Expr::BinaryOp {
940 left: Box::new(Expr::Literal(Value::Int(3))),
941 op: BinaryOp::LtEq,
942 right: Box::new(Expr::Literal(Value::Int(5))),
943 };
944 assert_eq!(eval_expr(&eq, &mut scope), Ok(Value::Bool(true)));
945 assert_eq!(eval_expr(<, &mut scope), Ok(Value::Bool(true)));
946 }
947
948 #[test]
949 fn eval_greater_than_or_equal() {
950 let mut scope = Scope::new();
951 let eq = Expr::BinaryOp {
952 left: Box::new(Expr::Literal(Value::Int(5))),
953 op: BinaryOp::GtEq,
954 right: Box::new(Expr::Literal(Value::Int(5))),
955 };
956 let gt = Expr::BinaryOp {
957 left: Box::new(Expr::Literal(Value::Int(7))),
958 op: BinaryOp::GtEq,
959 right: Box::new(Expr::Literal(Value::Int(5))),
960 };
961 assert_eq!(eval_expr(&eq, &mut scope), Ok(Value::Bool(true)));
962 assert_eq!(eval_expr(>, &mut scope), Ok(Value::Bool(true)));
963 }
964
965 #[test]
966 fn eval_string_comparison() {
967 let mut scope = Scope::new();
968 let expr = Expr::BinaryOp {
969 left: Box::new(Expr::Literal(Value::String("apple".into()))),
970 op: BinaryOp::Lt,
971 right: Box::new(Expr::Literal(Value::String("banana".into()))),
972 };
973 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
974 }
975
976 #[test]
977 fn eval_mixed_int_float_comparison() {
978 let mut scope = Scope::new();
979 let expr = Expr::BinaryOp {
980 left: Box::new(Expr::Literal(Value::Int(3))),
981 op: BinaryOp::Lt,
982 right: Box::new(Expr::Literal(Value::Float(3.5))),
983 };
984 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
985 }
986
987 #[test]
988 fn eval_int_float_equality() {
989 let mut scope = Scope::new();
990 let expr = Expr::BinaryOp {
991 left: Box::new(Expr::Literal(Value::Int(5))),
992 op: BinaryOp::Eq,
993 right: Box::new(Expr::Literal(Value::Float(5.0))),
994 };
995 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
996 }
997
998 #[test]
999 fn eval_type_mismatch_comparison() {
1000 let mut scope = Scope::new();
1001 let expr = Expr::BinaryOp {
1002 left: Box::new(Expr::Literal(Value::Int(5))),
1003 op: BinaryOp::Lt,
1004 right: Box::new(Expr::Literal(Value::String("five".into()))),
1005 };
1006 assert!(matches!(eval_expr(&expr, &mut scope), Err(EvalError::TypeError { .. })));
1007 }
1008
1009 #[test]
1010 fn is_truthy_values() {
1011 assert!(!is_truthy(&Value::Null));
1012 assert!(!is_truthy(&Value::Bool(false)));
1013 assert!(is_truthy(&Value::Bool(true)));
1014 assert!(!is_truthy(&Value::Int(0)));
1015 assert!(is_truthy(&Value::Int(1)));
1016 assert!(is_truthy(&Value::Int(-1)));
1017 assert!(!is_truthy(&Value::Float(0.0)));
1018 assert!(is_truthy(&Value::Float(0.1)));
1019 assert!(!is_truthy(&Value::String("".into())));
1020 assert!(is_truthy(&Value::String("x".into())));
1021 }
1022
1023 #[test]
1024 fn eval_command_subst_fails_without_executor() {
1025 use crate::ast::{Command, Pipeline};
1026
1027 let mut scope = Scope::new();
1028 let pipeline = Pipeline {
1029 commands: vec![Command {
1030 name: "echo".into(),
1031 args: vec![],
1032 redirects: vec![],
1033 }],
1034 background: false,
1035 };
1036 let expr = Expr::CommandSubst(Box::new(pipeline));
1037
1038 assert!(matches!(
1039 eval_expr(&expr, &mut scope),
1040 Err(EvalError::NoExecutor)
1041 ));
1042 }
1043
1044 #[test]
1045 fn eval_last_result_field() {
1046 let mut scope = Scope::new();
1047 scope.set_last_result(ExecResult::failure(42, "test error"));
1048
1049 let expr = Expr::VarRef(VarPath {
1051 segments: vec![
1052 VarSegment::Field("?".into()),
1053 VarSegment::Field("code".into()),
1054 ],
1055 });
1056 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1057
1058 let expr = Expr::VarRef(VarPath {
1060 segments: vec![
1061 VarSegment::Field("?".into()),
1062 VarSegment::Field("err".into()),
1063 ],
1064 });
1065 assert_eq!(
1066 eval_expr(&expr, &mut scope),
1067 Ok(Value::String("test error".into()))
1068 );
1069 }
1070
1071 #[test]
1072 fn value_to_string_all_types() {
1073 assert_eq!(value_to_string(&Value::Null), "null");
1074 assert_eq!(value_to_string(&Value::Bool(true)), "true");
1075 assert_eq!(value_to_string(&Value::Int(42)), "42");
1076 assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1077 assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1078 }
1079
1080 #[test]
1083 fn eval_negative_int() {
1084 let mut scope = Scope::new();
1085 let expr = Expr::Literal(Value::Int(-42));
1086 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1087 }
1088
1089 #[test]
1090 fn eval_negative_float() {
1091 let mut scope = Scope::new();
1092 let expr = Expr::Literal(Value::Float(-3.14));
1093 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1094 }
1095
1096 #[test]
1097 fn eval_zero_values() {
1098 let mut scope = Scope::new();
1099 assert_eq!(
1100 eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1101 Ok(Value::Int(0))
1102 );
1103 assert_eq!(
1104 eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1105 Ok(Value::Float(0.0))
1106 );
1107 }
1108
1109 #[test]
1110 fn eval_interpolation_empty_var() {
1111 let mut scope = Scope::new();
1112 scope.set("EMPTY", Value::String("".into()));
1113
1114 let expr = Expr::Interpolated(vec![
1115 StringPart::Literal("prefix".into()),
1116 StringPart::Var(VarPath::simple("EMPTY")),
1117 StringPart::Literal("suffix".into()),
1118 ]);
1119 assert_eq!(
1120 eval_expr(&expr, &mut scope),
1121 Ok(Value::String("prefixsuffix".into()))
1122 );
1123 }
1124
1125 #[test]
1126 fn eval_chained_and() {
1127 let mut scope = Scope::new();
1128 let expr = Expr::BinaryOp {
1130 left: Box::new(Expr::BinaryOp {
1131 left: Box::new(Expr::Literal(Value::Bool(true))),
1132 op: BinaryOp::And,
1133 right: Box::new(Expr::Literal(Value::Bool(true))),
1134 }),
1135 op: BinaryOp::And,
1136 right: Box::new(Expr::Literal(Value::Int(42))),
1137 };
1138 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1139 }
1140
1141 #[test]
1142 fn eval_chained_or() {
1143 let mut scope = Scope::new();
1144 let expr = Expr::BinaryOp {
1146 left: Box::new(Expr::BinaryOp {
1147 left: Box::new(Expr::Literal(Value::Bool(false))),
1148 op: BinaryOp::Or,
1149 right: Box::new(Expr::Literal(Value::Bool(false))),
1150 }),
1151 op: BinaryOp::Or,
1152 right: Box::new(Expr::Literal(Value::Int(42))),
1153 };
1154 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1155 }
1156
1157 #[test]
1158 fn eval_mixed_and_or() {
1159 let mut scope = Scope::new();
1160 let expr = Expr::BinaryOp {
1163 left: Box::new(Expr::BinaryOp {
1164 left: Box::new(Expr::Literal(Value::Bool(true))),
1165 op: BinaryOp::Or,
1166 right: Box::new(Expr::Literal(Value::Bool(false))),
1167 }),
1168 op: BinaryOp::And,
1169 right: Box::new(Expr::Literal(Value::Bool(true))),
1170 };
1171 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1173 }
1174
1175 #[test]
1176 fn eval_comparison_with_variables() {
1177 let mut scope = Scope::new();
1178 scope.set("X", Value::Int(10));
1179 scope.set("Y", Value::Int(5));
1180
1181 let expr = Expr::BinaryOp {
1182 left: Box::new(var_expr("X")),
1183 op: BinaryOp::Gt,
1184 right: Box::new(var_expr("Y")),
1185 };
1186 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1187 }
1188
1189 #[test]
1190 fn eval_string_equality() {
1191 let mut scope = Scope::new();
1192 let expr = Expr::BinaryOp {
1193 left: Box::new(Expr::Literal(Value::String("hello".into()))),
1194 op: BinaryOp::Eq,
1195 right: Box::new(Expr::Literal(Value::String("hello".into()))),
1196 };
1197 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1198 }
1199
1200 #[test]
1201 fn eval_string_inequality() {
1202 let mut scope = Scope::new();
1203 let expr = Expr::BinaryOp {
1204 left: Box::new(Expr::Literal(Value::String("hello".into()))),
1205 op: BinaryOp::NotEq,
1206 right: Box::new(Expr::Literal(Value::String("world".into()))),
1207 };
1208 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1209 }
1210
1211 #[test]
1212 fn eval_null_equality() {
1213 let mut scope = Scope::new();
1214 let expr = Expr::BinaryOp {
1215 left: Box::new(Expr::Literal(Value::Null)),
1216 op: BinaryOp::Eq,
1217 right: Box::new(Expr::Literal(Value::Null)),
1218 };
1219 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1220 }
1221
1222 #[test]
1223 fn eval_null_not_equal_to_int() {
1224 let mut scope = Scope::new();
1225 let expr = Expr::BinaryOp {
1226 left: Box::new(Expr::Literal(Value::Null)),
1227 op: BinaryOp::Eq,
1228 right: Box::new(Expr::Literal(Value::Int(0))),
1229 };
1230 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
1231 }
1232
1233 #[test]
1234 fn eval_float_comparison_boundary() {
1235 let mut scope = Scope::new();
1236 let expr = Expr::BinaryOp {
1238 left: Box::new(Expr::Literal(Value::Float(1.0))),
1239 op: BinaryOp::Eq,
1240 right: Box::new(Expr::Literal(Value::Float(1.0))),
1241 };
1242 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1243 }
1244
1245 #[test]
1246 fn eval_interpolation_with_bool() {
1247 let mut scope = Scope::new();
1248 scope.set("FLAG", Value::Bool(true));
1249
1250 let expr = Expr::Interpolated(vec![
1251 StringPart::Literal("enabled: ".into()),
1252 StringPart::Var(VarPath::simple("FLAG")),
1253 ]);
1254 assert_eq!(
1255 eval_expr(&expr, &mut scope),
1256 Ok(Value::String("enabled: true".into()))
1257 );
1258 }
1259
1260 #[test]
1261 fn eval_interpolation_with_null() {
1262 let mut scope = Scope::new();
1263 scope.set("VAL", Value::Null);
1264
1265 let expr = Expr::Interpolated(vec![
1266 StringPart::Literal("value: ".into()),
1267 StringPart::Var(VarPath::simple("VAL")),
1268 ]);
1269 assert_eq!(
1270 eval_expr(&expr, &mut scope),
1271 Ok(Value::String("value: null".into()))
1272 );
1273 }
1274
1275 #[test]
1276 fn eval_format_path_simple() {
1277 let path = VarPath::simple("X");
1278 assert_eq!(format_path(&path), "${X}");
1279 }
1280
1281 #[test]
1282 fn eval_format_path_nested() {
1283 let path = VarPath {
1284 segments: vec![
1285 VarSegment::Field("?".into()),
1286 VarSegment::Field("code".into()),
1287 ],
1288 };
1289 assert_eq!(format_path(&path), "${?.code}");
1290 }
1291
1292 #[test]
1293 fn type_name_all_types() {
1294 assert_eq!(type_name(&Value::Null), "null");
1295 assert_eq!(type_name(&Value::Bool(true)), "bool");
1296 assert_eq!(type_name(&Value::Int(1)), "int");
1297 assert_eq!(type_name(&Value::Float(1.0)), "float");
1298 assert_eq!(type_name(&Value::String("".into())), "string");
1299 }
1300
1301 #[test]
1302 fn expand_tilde_home() {
1303 if let Ok(home) = std::env::var("HOME") {
1305 assert_eq!(expand_tilde("~"), home);
1306 assert_eq!(expand_tilde("~/foo"), format!("{}/foo", home));
1307 assert_eq!(expand_tilde("~/foo/bar"), format!("{}/foo/bar", home));
1308 }
1309 }
1310
1311 #[test]
1312 fn expand_tilde_passthrough() {
1313 assert_eq!(expand_tilde("/home/user"), "/home/user");
1315 assert_eq!(expand_tilde("foo~bar"), "foo~bar");
1316 assert_eq!(expand_tilde(""), "");
1317 }
1318
1319 #[test]
1320 #[cfg(unix)]
1321 fn expand_tilde_user() {
1322 let expanded = expand_tilde("~root");
1324 assert!(
1326 expanded == "/root" || expanded == "/var/root",
1327 "expected /root or /var/root, got: {}",
1328 expanded
1329 );
1330
1331 let expanded_path = expand_tilde("~root/subdir");
1333 assert!(
1334 expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1335 "expected /root/subdir or /var/root/subdir, got: {}",
1336 expanded_path
1337 );
1338
1339 let nonexistent = expand_tilde("~nonexistent_user_12345");
1341 assert_eq!(nonexistent, "~nonexistent_user_12345");
1342 }
1343
1344 #[test]
1345 fn value_to_string_with_tilde_expansion() {
1346 if let Ok(home) = std::env::var("HOME") {
1347 let val = Value::String("~/test".into());
1348 assert_eq!(value_to_string_with_tilde(&val), format!("{}/test", home));
1349 }
1350 }
1351
1352 #[test]
1353 fn eval_positional_param() {
1354 let mut scope = Scope::new();
1355 scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1356
1357 let expr = Expr::Positional(0);
1359 let result = eval_expr(&expr, &mut scope).unwrap();
1360 assert_eq!(result, Value::String("my_tool".into()));
1361
1362 let expr = Expr::Positional(1);
1364 let result = eval_expr(&expr, &mut scope).unwrap();
1365 assert_eq!(result, Value::String("hello".into()));
1366
1367 let expr = Expr::Positional(2);
1369 let result = eval_expr(&expr, &mut scope).unwrap();
1370 assert_eq!(result, Value::String("world".into()));
1371
1372 let expr = Expr::Positional(3);
1374 let result = eval_expr(&expr, &mut scope).unwrap();
1375 assert_eq!(result, Value::String("".into()));
1376 }
1377
1378 #[test]
1379 fn eval_all_args() {
1380 let mut scope = Scope::new();
1381 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1382
1383 let expr = Expr::AllArgs;
1384 let result = eval_expr(&expr, &mut scope).unwrap();
1385
1386 assert_eq!(result, Value::String("a b c".into()));
1388 }
1389
1390 #[test]
1391 fn eval_arg_count() {
1392 let mut scope = Scope::new();
1393 scope.set_positional("test", vec!["x".into(), "y".into()]);
1394
1395 let expr = Expr::ArgCount;
1396 let result = eval_expr(&expr, &mut scope).unwrap();
1397 assert_eq!(result, Value::Int(2));
1398 }
1399
1400 #[test]
1401 fn eval_arg_count_empty() {
1402 let mut scope = Scope::new();
1403
1404 let expr = Expr::ArgCount;
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_string() {
1411 let mut scope = Scope::new();
1412 scope.set("NAME", Value::String("hello".into()));
1413
1414 let expr = Expr::VarLength("NAME".into());
1415 let result = eval_expr(&expr, &mut scope).unwrap();
1416 assert_eq!(result, Value::Int(5));
1417 }
1418
1419 #[test]
1420 fn eval_var_length_empty_string() {
1421 let mut scope = Scope::new();
1422 scope.set("EMPTY", Value::String("".into()));
1423
1424 let expr = Expr::VarLength("EMPTY".into());
1425 let result = eval_expr(&expr, &mut scope).unwrap();
1426 assert_eq!(result, Value::Int(0));
1427 }
1428
1429 #[test]
1430 fn eval_var_length_unset() {
1431 let mut scope = Scope::new();
1432
1433 let expr = Expr::VarLength("MISSING".into());
1435 let result = eval_expr(&expr, &mut scope).unwrap();
1436 assert_eq!(result, Value::Int(0));
1437 }
1438
1439 #[test]
1440 fn eval_var_length_int() {
1441 let mut scope = Scope::new();
1442 scope.set("NUM", Value::Int(12345));
1443
1444 let expr = Expr::VarLength("NUM".into());
1446 let result = eval_expr(&expr, &mut scope).unwrap();
1447 assert_eq!(result, Value::Int(5)); }
1449
1450 #[test]
1451 fn eval_var_with_default_set() {
1452 let mut scope = Scope::new();
1453 scope.set("NAME", Value::String("Alice".into()));
1454
1455 let expr = Expr::VarWithDefault {
1457 name: "NAME".into(),
1458 default: vec![StringPart::Literal("default".into())],
1459 };
1460 let result = eval_expr(&expr, &mut scope).unwrap();
1461 assert_eq!(result, Value::String("Alice".into()));
1462 }
1463
1464 #[test]
1465 fn eval_var_with_default_unset() {
1466 let mut scope = Scope::new();
1467
1468 let expr = Expr::VarWithDefault {
1470 name: "MISSING".into(),
1471 default: vec![StringPart::Literal("fallback".into())],
1472 };
1473 let result = eval_expr(&expr, &mut scope).unwrap();
1474 assert_eq!(result, Value::String("fallback".into()));
1475 }
1476
1477 #[test]
1478 fn eval_var_with_default_empty() {
1479 let mut scope = Scope::new();
1480 scope.set("EMPTY", Value::String("".into()));
1481
1482 let expr = Expr::VarWithDefault {
1484 name: "EMPTY".into(),
1485 default: vec![StringPart::Literal("not empty".into())],
1486 };
1487 let result = eval_expr(&expr, &mut scope).unwrap();
1488 assert_eq!(result, Value::String("not empty".into()));
1489 }
1490
1491 #[test]
1492 fn eval_var_with_default_non_string() {
1493 let mut scope = Scope::new();
1494 scope.set("NUM", Value::Int(42));
1495
1496 let expr = Expr::VarWithDefault {
1498 name: "NUM".into(),
1499 default: vec![StringPart::Literal("default".into())],
1500 };
1501 let result = eval_expr(&expr, &mut scope).unwrap();
1502 assert_eq!(result, Value::Int(42));
1503 }
1504
1505 #[test]
1506 fn eval_unset_variable_is_empty() {
1507 let mut scope = Scope::new();
1508 let parts = vec![
1509 StringPart::Literal("prefix:".into()),
1510 StringPart::Var(VarPath::simple("UNSET")),
1511 StringPart::Literal(":suffix".into()),
1512 ];
1513 let expr = Expr::Interpolated(parts);
1514 let result = eval_expr(&expr, &mut scope).unwrap();
1515 assert_eq!(result, Value::String("prefix::suffix".into()));
1516 }
1517
1518 #[test]
1519 fn eval_unset_variable_multiple() {
1520 let mut scope = Scope::new();
1521 scope.set("SET", Value::String("hello".into()));
1522 let parts = vec![
1523 StringPart::Var(VarPath::simple("UNSET1")),
1524 StringPart::Literal("-".into()),
1525 StringPart::Var(VarPath::simple("SET")),
1526 StringPart::Literal("-".into()),
1527 StringPart::Var(VarPath::simple("UNSET2")),
1528 ];
1529 let expr = Expr::Interpolated(parts);
1530 let result = eval_expr(&expr, &mut scope).unwrap();
1531 assert_eq!(result, Value::String("-hello-".into()));
1532 }
1533}