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