1use std::fmt;
13
14use kaish_types::json_to_value_no_envelope;
15
16use crate::arithmetic;
17use crate::ast::{
18 spread_non_list_message, BinaryOp, Expr, ListElem, RecordEntry, RecordKey,
19 StringPart, StringTestOp, TestCmpOp, TestExpr, Value, VarPath,
20};
21
22use super::scope::Scope;
23
24pub fn strip_leading_tabs(s: &str) -> String {
30 let mut out = String::with_capacity(s.len());
31 let mut at_line_start = true;
32 for ch in s.chars() {
33 if at_line_start && ch == '\t' {
34 continue;
36 }
37 out.push(ch);
38 at_line_start = ch == '\n';
39 }
40 out
41}
42
43pub struct HeredocAssembler {
57 out: String,
58 strip_tabs: bool,
59 at_line_start: bool,
60}
61
62impl HeredocAssembler {
63 pub fn new(strip_tabs: bool) -> Self {
64 Self {
65 out: String::new(),
66 strip_tabs,
67 at_line_start: true,
68 }
69 }
70
71 pub fn push_literal(&mut self, literal: &str) {
74 if !self.strip_tabs {
75 self.out.push_str(literal);
76 return;
77 }
78 for ch in literal.chars() {
79 match ch {
80 '\n' => {
81 self.out.push(ch);
82 self.at_line_start = true;
83 }
84 '\t' if self.at_line_start => {} _ => {
86 self.out.push(ch);
87 self.at_line_start = false;
88 }
89 }
90 }
91 }
92
93 pub fn push_interpolated(&mut self, value: &str) {
98 self.out.push_str(value);
99 if self.strip_tabs {
100 self.at_line_start = false;
101 }
102 }
103
104 pub fn into_string(self) -> String {
105 self.out
106 }
107}
108
109#[derive(Debug, Clone, PartialEq)]
111pub enum EvalError {
112 UndefinedVariable(String),
114 InvalidPath(String),
116 TypeError { expected: &'static str, got: String },
118 CommandFailed(String),
120 NoExecutor,
125 ArithmeticError(String),
127 RegexError(String),
129 Unsupported(String),
133}
134
135impl fmt::Display for EvalError {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 match self {
138 EvalError::UndefinedVariable(name) => write!(f, "undefined variable: {name}"),
139 EvalError::InvalidPath(path) => write!(f, "invalid path: {path}"),
140 EvalError::TypeError { expected, got } => {
141 write!(f, "type error: expected {expected}, got {got}")
142 }
143 EvalError::CommandFailed(msg) => write!(f, "command failed: {msg}"),
144 EvalError::NoExecutor => write!(
145 f,
146 "command substitution must be resolved by the async evaluator before sync evaluation"
147 ),
148 EvalError::ArithmeticError(msg) => write!(f, "arithmetic error: {msg}"),
149 EvalError::RegexError(msg) => write!(f, "regex error: {msg}"),
150 EvalError::Unsupported(msg) => write!(f, "{msg}"),
151 }
152 }
153}
154
155impl std::error::Error for EvalError {}
156
157pub type EvalResult<T> = Result<T, EvalError>;
159
160pub struct Evaluator<'a> {
167 scope: &'a mut Scope,
168}
169
170impl<'a> Evaluator<'a> {
171 pub fn new(scope: &'a mut Scope) -> Self {
173 Self { scope }
174 }
175
176 pub fn eval(&mut self, expr: &Expr) -> EvalResult<Value> {
178 match expr {
179 Expr::Literal(value) => self.eval_literal(value),
180 Expr::VarRef(path) => self.eval_var_ref(path),
181 Expr::Interpolated(parts) => self.eval_interpolated(parts),
182 Expr::HereDocBody { parts, strip_tabs } => {
183 let mut asm = HeredocAssembler::new(*strip_tabs);
186 for sp in parts {
187 match &sp.part {
188 StringPart::Literal(s) => asm.push_literal(s),
189 other => {
190 let value = self.eval_interpolated(std::slice::from_ref(other))?;
197 asm.push_interpolated(&value_to_text_sink(&value)?);
198 }
199 }
200 }
201 Ok(Value::String(asm.into_string()))
202 }
203 Expr::BinaryOp { left, op, right } => self.eval_binary_op(left, *op, right),
204 Expr::CommandSubst(_) => Err(EvalError::NoExecutor),
208 Expr::Test(test_expr) => self.eval_test(test_expr),
209 Expr::Positional(n) => self.eval_positional(*n),
210 Expr::AllArgs => self.eval_all_args(),
211 Expr::ArgCount => self.eval_arg_count(),
212 Expr::VarLength(path) => self.eval_var_length(path),
213 Expr::VarWithDefault { path, default } => self.eval_var_with_default(path, default),
214 Expr::Arithmetic(expr_str) => self.eval_arithmetic(expr_str),
215 Expr::Command(cmd) => self.eval_command(cmd),
216 Expr::LastExitCode => self.eval_last_exit_code(),
217 Expr::CurrentPid => self.eval_current_pid(),
218 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
219 Expr::ListLiteral(elems) => self.eval_list_literal(elems),
220 Expr::RecordLiteral(entries) => self.eval_record_literal(entries),
221 }
222 }
223
224 fn eval_list_literal(&mut self, elems: &[ListElem]) -> EvalResult<Value> {
228 let mut out = Vec::with_capacity(elems.len());
229 for elem in elems {
230 match elem {
231 ListElem::Item(e) => {
232 let value = self.eval(e)?;
233 out.push(kaish_types::value_to_json(&value));
234 }
235 ListElem::Spread(e) => {
236 let value = self.eval(e)?;
237 match value {
238 Value::Json(serde_json::Value::Array(items)) => out.extend(items),
239 other => return Err(EvalError::Unsupported(spread_non_list_message(&other))),
240 }
241 }
242 }
243 }
244 Ok(Value::Json(serde_json::Value::Array(out)))
245 }
246
247 fn eval_record_literal(&mut self, entries: &[RecordEntry]) -> EvalResult<Value> {
252 let mut map = serde_json::Map::new();
253 for entry in entries {
254 let key = match &entry.key {
255 RecordKey::Bare(s) | RecordKey::Quoted(s) => s.clone(),
256 RecordKey::Interpolated(parts) => {
263 value_to_text_sink(&self.eval_interpolated(parts)?)?
264 }
265 };
266 let value = self.eval(&entry.value)?;
267 map.insert(key, kaish_types::value_to_json(&value));
268 }
269 Ok(Value::Json(serde_json::Value::Object(map)))
270 }
271
272 fn eval_last_exit_code(&self) -> EvalResult<Value> {
274 Ok(Value::Int(self.scope.last_result().code))
275 }
276
277 fn eval_current_pid(&self) -> EvalResult<Value> {
279 Ok(Value::Int(self.scope.pid() as i64))
280 }
281
282 fn eval_command(&mut self, cmd: &crate::ast::Command) -> EvalResult<Value> {
284 match cmd.name.as_str() {
287 "true" => Ok(Value::Bool(true)),
288 "false" => Ok(Value::Bool(false)),
289 _ => Err(EvalError::NoExecutor),
293 }
294 }
295
296 fn eval_arithmetic(&mut self, expr_str: &str) -> EvalResult<Value> {
298 arithmetic::eval_arithmetic(expr_str, self.scope)
299 .map(Value::Int)
300 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
301 }
302
303 fn eval_test(&mut self, test_expr: &TestExpr) -> EvalResult<Value> {
305 let result = match test_expr {
306 TestExpr::FileTest { .. } => {
307 return Err(EvalError::Unsupported(
314 "file tests must be resolved by the async evaluator".to_string(),
315 ));
316 }
317 TestExpr::StringTest { op, value } => match op {
318 StringTestOp::IsEmpty | StringTestOp::IsNonEmpty => {
319 let val = self.eval(value)?;
320 let symbol = match op {
324 StringTestOp::IsEmpty => "-z",
325 StringTestOp::IsNonEmpty => "-n",
326 StringTestOp::IsList | StringTestOp::IsRecord => unreachable!(),
327 };
328 if let Some(msg) = scalar_test_operand_error(symbol, &val) {
329 return Err(EvalError::Unsupported(msg));
330 }
331 let s = value_to_string(&val);
332 match op {
333 StringTestOp::IsEmpty => s.is_empty(),
334 StringTestOp::IsNonEmpty => !s.is_empty(),
335 StringTestOp::IsList | StringTestOp::IsRecord => unreachable!(),
336 }
337 }
338 StringTestOp::IsList | StringTestOp::IsRecord => {
345 let val = self.eval(value)?;
346 op.matches_shape(&val)
347 }
348 },
349 TestExpr::Comparison { left, op, right } => {
350 let left_val = self.eval(left)?;
351 let right_val = self.eval(right)?;
352
353 match op {
354 TestCmpOp::Eq => values_equal(&left_val, &right_val)?,
355 TestCmpOp::NotEq => !(values_equal(&left_val, &right_val)?),
356 TestCmpOp::Match => {
357 guard_scalar_test_operands(op, &left_val, &right_val)?;
359 match regex_match(&left_val, &right_val, false)? {
361 Value::Bool(b) => b,
362 _ => false,
363 }
364 }
365 TestCmpOp::NotMatch => {
366 guard_scalar_test_operands(op, &left_val, &right_val)?;
367 match regex_match(&left_val, &right_val, true)? {
369 Value::Bool(b) => b,
370 _ => true,
371 }
372 }
373 TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
374 guard_scalar_test_operands(op, &left_val, &right_val)?;
376 let ord = compare_values(&left_val, &right_val)?;
378 match op {
379 TestCmpOp::Gt => ord.is_gt(),
380 TestCmpOp::Lt => ord.is_lt(),
381 TestCmpOp::GtEq => ord.is_ge(),
382 TestCmpOp::LtEq => ord.is_le(),
383 _ => unreachable!(),
384 }
385 }
386 TestCmpOp::NumEq
387 | TestCmpOp::NumNotEq
388 | TestCmpOp::NumGt
389 | TestCmpOp::NumLt
390 | TestCmpOp::NumGtEq
391 | TestCmpOp::NumLtEq => {
392 guard_scalar_test_operands(op, &left_val, &right_val)?;
394 let ord = numeric_compare(&left_val, &right_val)?;
397 match op {
398 TestCmpOp::NumEq => ord.is_eq(),
399 TestCmpOp::NumNotEq => !ord.is_eq(),
400 TestCmpOp::NumGt => ord.is_gt(),
401 TestCmpOp::NumLt => ord.is_lt(),
402 TestCmpOp::NumGtEq => ord.is_ge(),
403 TestCmpOp::NumLtEq => ord.is_le(),
404 _ => unreachable!(),
405 }
406 }
407 }
408 }
409 TestExpr::And { left, right } => {
410 let left_result = self.eval_test(left)?;
412 if !value_to_bool(&left_result) {
413 false } else {
415 value_to_bool(&self.eval_test(right)?)
416 }
417 }
418 TestExpr::Or { left, right } => {
419 let left_result = self.eval_test(left)?;
421 if value_to_bool(&left_result) {
422 true } else {
424 value_to_bool(&self.eval_test(right)?)
425 }
426 }
427 TestExpr::Not { expr } => {
428 let result = self.eval_test(expr)?;
429 !value_to_bool(&result)
430 }
431 TestExpr::In { left, right } => {
432 let left_val = self.eval(left)?;
433 let right_val = self.eval(right)?;
434 eval_membership(&left_val, &right_val)?
435 }
436 TestExpr::NotIn { left, right } => {
437 let left_val = self.eval(left)?;
438 let right_val = self.eval(right)?;
439 !eval_membership(&left_val, &right_val)?
440 }
441 };
442 Ok(Value::Bool(result))
443 }
444
445 fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
447 Ok(value.clone())
448 }
449
450 fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
452 match self.scope.resolve_path(path) {
453 Ok(v) => Ok(v),
454 Err(super::scope::PathError::UndefinedRoot(_)) => {
456 Err(EvalError::InvalidPath(format_path(path)))
457 }
458 Err(super::scope::PathError::Absence(msg))
461 | Err(super::scope::PathError::Shape(msg)) => Err(EvalError::InvalidPath(msg)),
462 }
463 }
464
465 fn eval_positional(&self, n: usize) -> EvalResult<Value> {
467 match self.scope.get_positional(n) {
468 Some(s) => Ok(Value::String(s.to_string())),
469 None => Ok(Value::String(String::new())), }
471 }
472
473 fn eval_all_args(&self) -> EvalResult<Value> {
477 let args = self.scope.all_args();
478 Ok(Value::String(args.join(" ")))
479 }
480
481 fn eval_arg_count(&self) -> EvalResult<Value> {
483 Ok(Value::Int(self.scope.arg_count() as i64))
484 }
485
486 fn eval_var_length(&self, path: &VarPath) -> EvalResult<Value> {
488 resolve_length(self.scope, path)
489 .map(Value::Int)
490 .map_err(EvalError::InvalidPath)
491 }
492
493 fn eval_var_with_default(&mut self, path: &VarPath, default: &[StringPart]) -> EvalResult<Value> {
497 match resolve_default(self.scope, path).map_err(EvalError::InvalidPath)? {
498 Some(value) => Ok(value),
499 None => self.eval_interpolated(default),
500 }
501 }
502
503 fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
505 let mut result = String::new();
506 for part in parts {
507 match part {
508 StringPart::Literal(s) => result.push_str(s),
509 StringPart::Var(path) => {
510 match self.scope.resolve_path(path) {
511 Ok(value) => result.push_str(&value_to_text_sink(&value)?),
513 Err(super::scope::PathError::UndefinedRoot(_)) => {}
515 Err(super::scope::PathError::Absence(msg))
518 | Err(super::scope::PathError::Shape(msg)) => {
519 return Err(EvalError::InvalidPath(msg))
520 }
521 }
522 }
523 StringPart::VarWithDefault { path, default } => {
524 let value = self.eval_var_with_default(path, default)?;
525 result.push_str(&value_to_text_sink(&value)?);
526 }
527 StringPart::VarLength(path) => {
528 let value = self.eval_var_length(path)?;
529 result.push_str(&value_to_text_sink(&value)?);
530 }
531 StringPart::Positional(n) => {
532 let value = self.eval_positional(*n)?;
533 result.push_str(&value_to_text_sink(&value)?);
534 }
535 StringPart::AllArgs => {
536 let value = self.eval_all_args()?;
537 result.push_str(&value_to_text_sink(&value)?);
538 }
539 StringPart::ArgCount => {
540 let value = self.eval_arg_count()?;
541 result.push_str(&value_to_text_sink(&value)?);
542 }
543 StringPart::Arithmetic(expr) => {
544 let value = self.eval_arithmetic_string(expr)?;
546 result.push_str(&value_to_text_sink(&value)?);
547 }
548 StringPart::CommandSubst(_) => {
549 return Err(EvalError::NoExecutor);
555 }
556 StringPart::LastExitCode => {
557 result.push_str(&self.scope.last_result().code.to_string());
558 }
559 StringPart::CurrentPid => {
560 result.push_str(&self.scope.pid().to_string());
561 }
562 }
563 }
564 Ok(Value::String(result))
565 }
566
567 fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
569 arithmetic::eval_arithmetic(expr, self.scope)
571 .map(Value::Int)
572 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
573 }
574
575 fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
579 match op {
580 BinaryOp::And => {
581 let left_val = self.eval(left)?;
582 if !is_truthy(&left_val) {
583 return Ok(left_val);
584 }
585 self.eval(right)
586 }
587 BinaryOp::Or => {
588 let left_val = self.eval(left)?;
589 if is_truthy(&left_val) {
590 return Ok(left_val);
591 }
592 self.eval(right)
593 }
594 }
595 }
596
597}
598
599pub fn value_to_exit_code(value: &Value) -> anyhow::Result<i64> {
606 match value {
607 Value::Int(n) => Ok(*n),
608 Value::Bool(b) => Ok(if *b { 0 } else { 1 }),
609 Value::Float(f) => Ok(*f as i64),
610 Value::String(s) => {
611 let trimmed = s.trim();
612 trimmed.parse::<i64>().map_err(|_| {
613 anyhow::anyhow!("numeric argument required: {:?}", s)
614 })
615 }
616 Value::Null | Value::Json(_) | Value::Bytes(_) => {
617 anyhow::bail!("numeric argument required (got {:?})", value)
618 }
619 }
620}
621
622pub fn value_length(value: &Value) -> i64 {
627 match value {
628 Value::Json(serde_json::Value::Array(a)) => a.len() as i64,
629 Value::Json(serde_json::Value::Object(o)) => o.len() as i64,
630 Value::Bytes(b) => b.len() as i64,
632 other => value_to_string(other).len() as i64,
633 }
634}
635
636pub fn value_defaults_on_emptiness(value: &Value) -> bool {
643 match value {
644 Value::Null | Value::Json(serde_json::Value::Null) => true,
645 Value::String(s) => s.is_empty(),
646 _ => false,
647 }
648}
649
650pub fn resolve_length(scope: &Scope, path: &VarPath) -> Result<i64, String> {
657 match scope.resolve_path(path) {
658 Ok(value) => Ok(value_length(&value)),
659 Err(super::scope::PathError::UndefinedRoot(_)) if path.segments.len() <= 1 => Ok(0),
660 Err(super::scope::PathError::UndefinedRoot(_)) => {
661 Err(format!("{}: undefined variable", format_path(path)))
662 }
663 Err(super::scope::PathError::Absence(msg)) | Err(super::scope::PathError::Shape(msg)) => {
664 Err(msg)
665 }
666 }
667}
668
669pub fn resolve_default(scope: &Scope, path: &VarPath) -> Result<Option<Value>, String> {
675 match scope.resolve_path(path) {
676 Ok(value) if value_defaults_on_emptiness(&value) => Ok(None),
677 Ok(value) => Ok(Some(value)),
678 Err(super::scope::PathError::UndefinedRoot(_))
679 | Err(super::scope::PathError::Absence(_)) => Ok(None),
680 Err(super::scope::PathError::Shape(msg)) => Err(msg),
681 }
682}
683
684pub fn structured_export_error(vars: &[(String, Value)]) -> Option<String> {
690 for (name, value) in vars {
691 if let Value::Json(j) = value {
692 if matches!(j, serde_json::Value::Array(_) | serde_json::Value::Object(_)) {
693 let kind = if j.is_array() { "list" } else { "record" };
694 return Some(format!(
695 "cannot export '{name}': it holds a {kind}, which can't be an OS environment variable — serialize it explicitly first, e.g. `export {name}=$(tojson ${name})`"
696 ));
697 }
698 }
699 }
700 None
701}
702
703pub fn is_collection(value: &Value) -> bool {
708 matches!(
709 value,
710 Value::Json(serde_json::Value::Array(_)) | Value::Json(serde_json::Value::Object(_))
711 )
712}
713
714fn collection_kind(value: &Value) -> &'static str {
717 match value {
718 Value::Json(serde_json::Value::Array(_)) => "list",
719 Value::Json(serde_json::Value::Object(_)) => "record",
720 _ => "collection",
721 }
722}
723
724pub fn structured_boundary_error(sink: &str, value: &Value) -> Option<String> {
734 if is_collection(value) {
735 let kind = collection_kind(value);
736 Some(format!(
737 "cannot use a {kind} as {sink} — serialize it explicitly first, e.g. `cmd $(tojson $x)`"
738 ))
739 } else {
740 None
741 }
742}
743
744pub fn scalar_test_operand_error(op_symbol: &str, value: &Value) -> Option<String> {
753 if is_collection(value) {
754 let kind = collection_kind(value);
755 Some(format!(
756 "`{op_symbol}` needs a scalar; got a {kind} — use `${{#x}}` for length, \
757 `-list`/`-record` to test shape, or `in` for membership"
758 ))
759 } else {
760 None
761 }
762}
763
764pub fn value_to_string(value: &Value) -> String {
765 match value {
766 Value::Null => "null".to_string(),
767 Value::Bool(b) => b.to_string(),
768 Value::Int(i) => i.to_string(),
769 Value::Float(f) => f.to_string(),
770 Value::String(s) => s.clone(),
771 Value::Json(json) => json.to_string(),
772 Value::Bytes(b) => format!("[binary: {} bytes]", b.len()),
778 }
779}
780
781pub fn value_to_text_sink(value: &Value) -> EvalResult<String> {
799 value_to_text_sink_named(value, "text")
800}
801
802pub fn value_to_text_sink_named(value: &Value, sink: &str) -> EvalResult<String> {
811 match value {
812 Value::Bytes(b) => match std::str::from_utf8(b) {
813 Ok(s) => Ok(s.to_string()),
814 Err(_) => Err(EvalError::Unsupported(format!(
815 "binary data ({} bytes) cannot be used as {sink} — decode it \
816 (base64/xxd) or redirect to a file",
817 b.len()
818 ))),
819 },
820 other => Ok(value_to_string(other)),
821 }
822}
823
824pub fn values_to_text_sink_named(values: &[Value], sink: &str) -> EvalResult<Vec<String>> {
828 values.iter().map(|v| value_to_text_sink_named(v, sink)).collect()
829}
830
831pub fn value_to_bool(value: &Value) -> bool {
841 match value {
842 Value::Null => false,
843 Value::Bool(b) => *b,
844 Value::Int(i) => *i != 0,
845 Value::Float(f) => *f != 0.0,
846 Value::String(s) => !s.is_empty(),
847 Value::Json(json) => match json {
848 serde_json::Value::Null => false,
849 serde_json::Value::Array(arr) => !arr.is_empty(),
850 serde_json::Value::Object(obj) => !obj.is_empty(),
851 serde_json::Value::Bool(b) => *b,
852 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
853 serde_json::Value::String(s) => !s.is_empty(),
854 },
855 Value::Bytes(b) => !b.is_empty(), }
857}
858
859pub fn expand_tilde(s: &str, home: Option<&str>) -> String {
873 if s == "~" {
874 home.map(|h| h.to_string()).unwrap_or_else(|| "~".to_string())
875 } else if s.starts_with("~/") {
876 match home {
877 Some(home) => format!("{}{}", home, &s[1..]),
878 None => s.to_string(),
879 }
880 } else if s.starts_with('~') {
881 expand_tilde_user(s)
883 } else {
884 s.to_string()
885 }
886}
887
888#[cfg(all(unix, feature = "host"))]
893fn expand_tilde_user(s: &str) -> String {
894 let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
896 (&s[1..slash_pos + 1], &s[slash_pos + 1..])
897 } else {
898 (&s[1..], "")
899 };
900
901 if username.is_empty() {
902 return s.to_string();
903 }
904
905 let passwd = match std::fs::read_to_string("/etc/passwd") {
908 Ok(content) => content,
909 Err(_) => return s.to_string(),
910 };
911
912 for line in passwd.lines() {
913 let fields: Vec<&str> = line.split(':').collect();
914 if fields.len() >= 6 && fields[0] == username {
915 let home_dir = fields[5];
916 return if rest.is_empty() {
917 home_dir.to_string()
918 } else {
919 format!("{}{}", home_dir, rest)
920 };
921 }
922 }
923
924 s.to_string()
926}
927
928#[cfg(not(all(unix, feature = "host")))]
929fn expand_tilde_user(s: &str) -> String {
930 s.to_string()
933}
934
935pub fn value_to_string_with_tilde(value: &Value, home: Option<&str>) -> String {
940 match value {
941 Value::String(s) if s.starts_with('~') => expand_tilde(s, home),
942 _ => value_to_string(value),
943 }
944}
945
946pub(crate) fn format_path(path: &VarPath) -> String {
951 use crate::ast::VarSegment;
952 let mut result = String::from("${");
953 for (i, seg) in path.segments.iter().enumerate() {
954 match seg {
955 VarSegment::Field(name) => {
956 if i > 0 {
957 result.push('.');
958 }
959 result.push_str(name);
960 }
961 VarSegment::Index(idx) => result.push_str(&format!("[{idx}]")),
962 VarSegment::Key(k) => result.push_str(&format!("[{k}]")),
963 VarSegment::Dynamic(v) => result.push_str(&format!("[${v}]")),
964 VarSegment::Slice(a, b) => {
965 let s = a.map(|n| n.to_string()).unwrap_or_default();
966 let e = b.map(|n| n.to_string()).unwrap_or_default();
967 result.push_str(&format!("[{s}:{e}]"));
968 }
969 }
970 }
971 result.push('}');
972 result
973}
974
975fn is_truthy(value: &Value) -> bool {
985 value_to_bool(value)
987}
988
989pub fn values_equal(left: &Value, right: &Value) -> EvalResult<bool> {
1000 match (left, right) {
1001 (Value::Null, Value::Null) => Ok(true),
1002 (Value::Bool(a), Value::Bool(b)) => Ok(a == b),
1003 (Value::Int(a), Value::Int(b)) => Ok(a == b),
1004 (Value::Float(a), Value::Float(b)) => Ok((a - b).abs() < f64::EPSILON),
1005 (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
1006 Ok((*a as f64 - b).abs() < f64::EPSILON)
1007 }
1008 (Value::String(a), Value::String(b)) => Ok(a == b),
1009 (Value::Json(a), Value::Json(b)) => Ok(a == b),
1010 (Value::Bytes(a), Value::Bytes(b)) => Ok(a == b),
1011 (Value::Json(j), other) | (other, Value::Json(j))
1017 if matches!(j, serde_json::Value::Array(_) | serde_json::Value::Object(_)) =>
1018 {
1019 let kind = if j.is_array() { "list" } else { "record" };
1020 Err(EvalError::Unsupported(format!(
1021 "cannot compare a {kind} to a {other_kind} with ==/!= — test membership with `[[ x in $coll ]]`, or compare structures with `jq`",
1022 other_kind = type_name(other),
1023 )))
1024 }
1025 (Value::Bytes(b), other) | (other, Value::Bytes(b)) => Err(EvalError::Unsupported(format!(
1031 "binary data ({} bytes) cannot be used as an ==/!= operand against a {} — decode it \
1032 first (base64/xxd), or compare two binary values directly",
1033 b.len(),
1034 type_name(other),
1035 ))),
1036 _ => Ok(value_to_string(left) == value_to_string(right)),
1039 }
1040}
1041
1042fn element_matches(needle: &Value, element: &Value) -> bool {
1051 match (needle, element) {
1052 (Value::Json(a), Value::Json(b)) => a == b,
1053 (Value::Json(_), _) | (_, Value::Json(_)) => false,
1054 _ => values_equal(needle, element).unwrap_or(false),
1061 }
1062}
1063
1064fn eval_membership(needle: &Value, haystack: &Value) -> EvalResult<bool> {
1074 match haystack {
1075 Value::Json(serde_json::Value::Array(items)) => {
1076 for item in items {
1077 let element = json_to_value_no_envelope(item.clone());
1078 if element_matches(needle, &element) {
1079 return Ok(true);
1080 }
1081 }
1082 Ok(false)
1083 }
1084 Value::Json(serde_json::Value::Object(map)) => {
1085 if let Value::Bytes(b) = needle {
1090 return Err(EvalError::Unsupported(format!(
1091 "binary data ({} bytes) cannot be used as a record key for `in` — \
1092 decode it first (base64/xxd)",
1093 b.len()
1094 )));
1095 }
1096 Ok(map.contains_key(&value_to_string(needle)))
1097 }
1098 other => Err(EvalError::Unsupported(format!(
1099 "`in` requires a list or record on the right-hand side, got {} — substring tests use `=~`, glob (`[[ $s == *sub* ]]`), or `case`",
1100 type_name(other),
1101 ))),
1102 }
1103}
1104
1105fn cmp_op_symbol(op: &TestCmpOp) -> &'static str {
1108 match op {
1109 TestCmpOp::Eq => "==",
1110 TestCmpOp::NotEq => "!=",
1111 TestCmpOp::Match => "=~",
1112 TestCmpOp::NotMatch => "!~",
1113 TestCmpOp::Gt => ">",
1114 TestCmpOp::Lt => "<",
1115 TestCmpOp::GtEq => ">=",
1116 TestCmpOp::LtEq => "<=",
1117 TestCmpOp::NumEq => "-eq",
1118 TestCmpOp::NumNotEq => "-ne",
1119 TestCmpOp::NumGt => "-gt",
1120 TestCmpOp::NumLt => "-lt",
1121 TestCmpOp::NumGtEq => "-ge",
1122 TestCmpOp::NumLtEq => "-le",
1123 }
1124}
1125
1126fn guard_scalar_test_operands(op: &TestCmpOp, left: &Value, right: &Value) -> EvalResult<()> {
1130 let symbol = cmp_op_symbol(op);
1131 if let Some(msg) = scalar_test_operand_error(symbol, left) {
1132 return Err(EvalError::Unsupported(msg));
1133 }
1134 if let Some(msg) = scalar_test_operand_error(symbol, right) {
1135 return Err(EvalError::Unsupported(msg));
1136 }
1137 Ok(())
1138}
1139
1140fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1142 match (left, right) {
1143 (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
1144 (Value::Float(a), Value::Float(b)) => {
1145 a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1146 }
1147 (Value::Int(a), Value::Float(b)) => {
1148 (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1149 }
1150 (Value::Float(a), Value::Int(b)) => {
1151 a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1152 }
1153 (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
1154 _ => Err(EvalError::TypeError {
1155 expected: "comparable types (numbers or strings)",
1156 got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
1157 }),
1158 }
1159}
1160
1161enum Num {
1166 Int(i64),
1167 Float(f64),
1168}
1169
1170fn value_to_num(value: &Value) -> EvalResult<Num> {
1171 match value {
1172 Value::Int(n) => Ok(Num::Int(*n)),
1173 Value::Float(f) => Ok(Num::Float(*f)),
1174 Value::String(s) => {
1175 let t = s.trim();
1176 if let Ok(n) = t.parse::<i64>() {
1177 Ok(Num::Int(n))
1178 } else if let Ok(f) = t.parse::<f64>() {
1179 Ok(Num::Float(f))
1180 } else {
1181 Err(EvalError::TypeError {
1182 expected: "numeric operand",
1183 got: format!("non-numeric string {:?}", s),
1184 })
1185 }
1186 }
1187 _ => Err(EvalError::TypeError {
1188 expected: "numeric operand",
1189 got: type_name(value).to_string(),
1190 }),
1191 }
1192}
1193
1194pub fn numeric_compare(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1199 let l = value_to_num(left)?;
1200 let r = value_to_num(right)?;
1201 match (l, r) {
1202 (Num::Int(a), Num::Int(b)) => Ok(a.cmp(&b)),
1203 (Num::Float(a), Num::Float(b)) => a
1204 .partial_cmp(&b)
1205 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1206 (Num::Int(a), Num::Float(b)) => (a as f64)
1207 .partial_cmp(&b)
1208 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1209 (Num::Float(a), Num::Int(b)) => a
1210 .partial_cmp(&(b as f64))
1211 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1212 }
1213}
1214
1215fn type_name(value: &Value) -> &'static str {
1217 match value {
1218 Value::Null => "null",
1219 Value::Bool(_) => "bool",
1220 Value::Int(_) => "int",
1221 Value::Float(_) => "float",
1222 Value::String(_) => "string",
1223 Value::Json(_) => "json",
1224 Value::Bytes(_) => "bytes",
1225 }
1226}
1227
1228fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
1233 let text = match left {
1234 Value::String(s) => s.as_str(),
1235 _ => {
1236 return Err(EvalError::TypeError {
1237 expected: "string",
1238 got: type_name(left).to_string(),
1239 })
1240 }
1241 };
1242
1243 let pattern = match right {
1244 Value::String(s) => s.as_str(),
1245 _ => {
1246 return Err(EvalError::TypeError {
1247 expected: "string (regex pattern)",
1248 got: type_name(right).to_string(),
1249 })
1250 }
1251 };
1252
1253 let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
1254 let matches = re.is_match(text);
1255
1256 Ok(Value::Bool(if negate { !matches } else { matches }))
1257}
1258
1259pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
1266 let mut evaluator = Evaluator::new(scope);
1267 evaluator.eval(expr)
1268}
1269
1270#[cfg(test)]
1271#[allow(clippy::approx_constant)]
1272mod tests {
1273 use super::*;
1274 use crate::ast::{Stmt, VarSegment};
1275 use super::super::result::ExecResult;
1276
1277 fn var_expr(name: &str) -> Expr {
1279 Expr::VarRef(VarPath::simple(name))
1280 }
1281
1282 #[test]
1283 fn eval_literal_int() {
1284 let mut scope = Scope::new();
1285 let expr = Expr::Literal(Value::Int(42));
1286 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1287 }
1288
1289 #[test]
1290 fn eval_literal_string() {
1291 let mut scope = Scope::new();
1292 let expr = Expr::Literal(Value::String("hello".into()));
1293 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
1294 }
1295
1296 #[test]
1297 fn eval_literal_bool() {
1298 let mut scope = Scope::new();
1299 assert_eq!(
1300 eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
1301 Ok(Value::Bool(true))
1302 );
1303 }
1304
1305 #[test]
1306 fn eval_literal_null() {
1307 let mut scope = Scope::new();
1308 assert_eq!(
1309 eval_expr(&Expr::Literal(Value::Null), &mut scope),
1310 Ok(Value::Null)
1311 );
1312 }
1313
1314 #[test]
1315 fn eval_literal_float() {
1316 let mut scope = Scope::new();
1317 let expr = Expr::Literal(Value::Float(3.14));
1318 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
1319 }
1320
1321 #[test]
1322 fn eval_variable_ref() {
1323 let mut scope = Scope::new();
1324 scope.set("X", Value::Int(100));
1325 assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
1326 }
1327
1328 #[test]
1329 fn eval_undefined_variable() {
1330 let mut scope = Scope::new();
1331 let result = eval_expr(&var_expr("MISSING"), &mut scope);
1332 assert!(matches!(result, Err(EvalError::InvalidPath(_))));
1333 }
1334
1335 #[test]
1336 fn eval_interpolated_string() {
1337 let mut scope = Scope::new();
1338 scope.set("NAME", Value::String("World".into()));
1339
1340 let expr = Expr::Interpolated(vec![
1341 StringPart::Literal("Hello, ".into()),
1342 StringPart::Var(VarPath::simple("NAME")),
1343 StringPart::Literal("!".into()),
1344 ]);
1345 assert_eq!(
1346 eval_expr(&expr, &mut scope),
1347 Ok(Value::String("Hello, World!".into()))
1348 );
1349 }
1350
1351 #[test]
1363 fn eval_heredoc_body_binary_var_is_loud() {
1364 let mut scope = Scope::new();
1365 scope.set("B", Value::Bytes(vec![0xff, 0x00, 0xfe]));
1366
1367 let expr = Expr::HereDocBody {
1368 parts: vec![
1369 crate::ast::SpannedPart {
1370 part: StringPart::Literal("before ".into()),
1371 offset: 0,
1372 len: 0,
1373 },
1374 crate::ast::SpannedPart {
1375 part: StringPart::Var(VarPath::simple("B")),
1376 offset: 0,
1377 len: 0,
1378 },
1379 ],
1380 strip_tabs: false,
1381 };
1382 let err = eval_expr(&expr, &mut scope).expect_err("binary in a heredoc body must be loud");
1383 assert!(
1384 matches!(err, EvalError::Unsupported(ref msg) if msg.contains("cannot be used as")),
1385 "got {err:?}"
1386 );
1387 }
1388
1389 #[test]
1390 fn eval_heredoc_body_text_var_is_unaffected() {
1391 let mut scope = Scope::new();
1392 scope.set("NAME", Value::String("World".into()));
1393
1394 let expr = Expr::HereDocBody {
1395 parts: vec![
1396 crate::ast::SpannedPart {
1397 part: StringPart::Literal("Hello, ".into()),
1398 offset: 0,
1399 len: 0,
1400 },
1401 crate::ast::SpannedPart {
1402 part: StringPart::Var(VarPath::simple("NAME")),
1403 offset: 0,
1404 len: 0,
1405 },
1406 ],
1407 strip_tabs: false,
1408 };
1409 assert_eq!(
1410 eval_expr(&expr, &mut scope),
1411 Ok(Value::String("Hello, World".into()))
1412 );
1413 }
1414
1415 #[test]
1416 fn eval_record_literal_interpolated_key_binary_var_is_loud() {
1417 let mut scope = Scope::new();
1418 scope.set("B", Value::Bytes(vec![0xff, 0x00, 0xfe]));
1419
1420 let expr = Expr::RecordLiteral(vec![RecordEntry {
1421 key: RecordKey::Interpolated(vec![StringPart::Var(VarPath::simple("B"))]),
1422 value: Expr::Literal(Value::Int(1)),
1423 }]);
1424 let err = eval_expr(&expr, &mut scope)
1425 .expect_err("a binary record key must be loud, not a `[binary: N bytes]` key");
1426 assert!(
1427 matches!(err, EvalError::Unsupported(ref msg) if msg.contains("cannot be used as")),
1428 "got {err:?}"
1429 );
1430 }
1431
1432 #[test]
1433 fn eval_record_literal_interpolated_key_text_var_is_unaffected() {
1434 let mut scope = Scope::new();
1435 scope.set("K", Value::String("port".into()));
1436
1437 let expr = Expr::RecordLiteral(vec![RecordEntry {
1438 key: RecordKey::Interpolated(vec![StringPart::Var(VarPath::simple("K"))]),
1439 value: Expr::Literal(Value::Int(8080)),
1440 }]);
1441 assert_eq!(
1442 eval_expr(&expr, &mut scope),
1443 Ok(Value::Json(serde_json::json!({"port": 8080})))
1444 );
1445 }
1446
1447 #[test]
1448 fn eval_interpolated_with_number() {
1449 let mut scope = Scope::new();
1450 scope.set("COUNT", Value::Int(42));
1451
1452 let expr = Expr::Interpolated(vec![
1453 StringPart::Literal("Count: ".into()),
1454 StringPart::Var(VarPath::simple("COUNT")),
1455 ]);
1456 assert_eq!(
1457 eval_expr(&expr, &mut scope),
1458 Ok(Value::String("Count: 42".into()))
1459 );
1460 }
1461
1462 #[test]
1463 fn eval_and_short_circuit_true() {
1464 let mut scope = Scope::new();
1465 let expr = Expr::BinaryOp {
1466 left: Box::new(Expr::Literal(Value::Bool(true))),
1467 op: BinaryOp::And,
1468 right: Box::new(Expr::Literal(Value::Int(42))),
1469 };
1470 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1472 }
1473
1474 #[test]
1475 fn eval_and_short_circuit_false() {
1476 let mut scope = Scope::new();
1477 let expr = Expr::BinaryOp {
1478 left: Box::new(Expr::Literal(Value::Bool(false))),
1479 op: BinaryOp::And,
1480 right: Box::new(Expr::Literal(Value::Int(42))),
1481 };
1482 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
1484 }
1485
1486 #[test]
1487 fn eval_or_short_circuit_true() {
1488 let mut scope = Scope::new();
1489 let expr = Expr::BinaryOp {
1490 left: Box::new(Expr::Literal(Value::Bool(true))),
1491 op: BinaryOp::Or,
1492 right: Box::new(Expr::Literal(Value::Int(42))),
1493 };
1494 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1496 }
1497
1498 #[test]
1499 fn eval_or_short_circuit_false() {
1500 let mut scope = Scope::new();
1501 let expr = Expr::BinaryOp {
1502 left: Box::new(Expr::Literal(Value::Bool(false))),
1503 op: BinaryOp::Or,
1504 right: Box::new(Expr::Literal(Value::Int(42))),
1505 };
1506 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1508 }
1509
1510 #[test]
1511 fn is_truthy_values() {
1512 assert!(!is_truthy(&Value::Null));
1513 assert!(!is_truthy(&Value::Bool(false)));
1514 assert!(is_truthy(&Value::Bool(true)));
1515 assert!(!is_truthy(&Value::Int(0)));
1516 assert!(is_truthy(&Value::Int(1)));
1517 assert!(is_truthy(&Value::Int(-1)));
1518 assert!(!is_truthy(&Value::Float(0.0)));
1519 assert!(is_truthy(&Value::Float(0.1)));
1520 assert!(!is_truthy(&Value::String("".into())));
1521 assert!(is_truthy(&Value::String("x".into())));
1522 }
1523
1524 #[test]
1525 fn sync_command_subst_is_loud_not_silent() {
1526 use crate::ast::Command;
1530
1531 let mut scope = Scope::new();
1532 let expr = Expr::CommandSubst(vec![Stmt::Command(Command {
1533 name: "echo".into(),
1534 args: vec![],
1535 redirects: vec![],
1536 })]);
1537
1538 assert!(matches!(
1539 eval_expr(&expr, &mut scope),
1540 Err(EvalError::NoExecutor)
1541 ));
1542 }
1543
1544 #[test]
1545 fn eval_last_result_bare() {
1546 let mut scope = Scope::new();
1549 scope.set_last_result(ExecResult::failure(42, "test error"));
1550
1551 let expr = Expr::VarRef(VarPath {
1552 segments: vec![VarSegment::Field("?".into())],
1553 });
1554 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1555 }
1556
1557 #[test]
1558 fn value_to_string_all_types() {
1559 assert_eq!(value_to_string(&Value::Null), "null");
1560 assert_eq!(value_to_string(&Value::Bool(true)), "true");
1561 assert_eq!(value_to_string(&Value::Int(42)), "42");
1562 assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1563 assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1564 }
1565
1566 #[test]
1569 fn eval_negative_int() {
1570 let mut scope = Scope::new();
1571 let expr = Expr::Literal(Value::Int(-42));
1572 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1573 }
1574
1575 #[test]
1576 fn eval_negative_float() {
1577 let mut scope = Scope::new();
1578 let expr = Expr::Literal(Value::Float(-3.14));
1579 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1580 }
1581
1582 #[test]
1583 fn eval_zero_values() {
1584 let mut scope = Scope::new();
1585 assert_eq!(
1586 eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1587 Ok(Value::Int(0))
1588 );
1589 assert_eq!(
1590 eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1591 Ok(Value::Float(0.0))
1592 );
1593 }
1594
1595 #[test]
1596 fn eval_interpolation_empty_var() {
1597 let mut scope = Scope::new();
1598 scope.set("EMPTY", Value::String("".into()));
1599
1600 let expr = Expr::Interpolated(vec![
1601 StringPart::Literal("prefix".into()),
1602 StringPart::Var(VarPath::simple("EMPTY")),
1603 StringPart::Literal("suffix".into()),
1604 ]);
1605 assert_eq!(
1606 eval_expr(&expr, &mut scope),
1607 Ok(Value::String("prefixsuffix".into()))
1608 );
1609 }
1610
1611 #[test]
1612 fn eval_chained_and() {
1613 let mut scope = Scope::new();
1614 let expr = Expr::BinaryOp {
1616 left: Box::new(Expr::BinaryOp {
1617 left: Box::new(Expr::Literal(Value::Bool(true))),
1618 op: BinaryOp::And,
1619 right: Box::new(Expr::Literal(Value::Bool(true))),
1620 }),
1621 op: BinaryOp::And,
1622 right: Box::new(Expr::Literal(Value::Int(42))),
1623 };
1624 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1625 }
1626
1627 #[test]
1628 fn eval_chained_or() {
1629 let mut scope = Scope::new();
1630 let expr = Expr::BinaryOp {
1632 left: Box::new(Expr::BinaryOp {
1633 left: Box::new(Expr::Literal(Value::Bool(false))),
1634 op: BinaryOp::Or,
1635 right: Box::new(Expr::Literal(Value::Bool(false))),
1636 }),
1637 op: BinaryOp::Or,
1638 right: Box::new(Expr::Literal(Value::Int(42))),
1639 };
1640 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1641 }
1642
1643 #[test]
1644 fn eval_mixed_and_or() {
1645 let mut scope = Scope::new();
1646 let expr = Expr::BinaryOp {
1649 left: Box::new(Expr::BinaryOp {
1650 left: Box::new(Expr::Literal(Value::Bool(true))),
1651 op: BinaryOp::Or,
1652 right: Box::new(Expr::Literal(Value::Bool(false))),
1653 }),
1654 op: BinaryOp::And,
1655 right: Box::new(Expr::Literal(Value::Bool(true))),
1656 };
1657 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1659 }
1660
1661 #[test]
1662 fn eval_interpolation_with_bool() {
1663 let mut scope = Scope::new();
1664 scope.set("FLAG", Value::Bool(true));
1665
1666 let expr = Expr::Interpolated(vec![
1667 StringPart::Literal("enabled: ".into()),
1668 StringPart::Var(VarPath::simple("FLAG")),
1669 ]);
1670 assert_eq!(
1671 eval_expr(&expr, &mut scope),
1672 Ok(Value::String("enabled: true".into()))
1673 );
1674 }
1675
1676 #[test]
1677 fn eval_interpolation_with_null() {
1678 let mut scope = Scope::new();
1679 scope.set("VAL", Value::Null);
1680
1681 let expr = Expr::Interpolated(vec![
1682 StringPart::Literal("value: ".into()),
1683 StringPart::Var(VarPath::simple("VAL")),
1684 ]);
1685 assert_eq!(
1686 eval_expr(&expr, &mut scope),
1687 Ok(Value::String("value: null".into()))
1688 );
1689 }
1690
1691 #[test]
1692 fn eval_format_path_simple() {
1693 let path = VarPath::simple("X");
1694 assert_eq!(format_path(&path), "${X}");
1695 }
1696
1697 #[test]
1698 fn eval_format_path_nested() {
1699 let path = VarPath {
1700 segments: vec![
1701 VarSegment::Field("X".into()),
1702 VarSegment::Field("field".into()),
1703 ],
1704 };
1705 assert_eq!(format_path(&path), "${X.field}");
1706 }
1707
1708 #[test]
1709 fn type_name_all_types() {
1710 assert_eq!(type_name(&Value::Null), "null");
1711 assert_eq!(type_name(&Value::Bool(true)), "bool");
1712 assert_eq!(type_name(&Value::Int(1)), "int");
1713 assert_eq!(type_name(&Value::Float(1.0)), "float");
1714 assert_eq!(type_name(&Value::String("".into())), "string");
1715 }
1716
1717 #[test]
1718 fn expand_tilde_home() {
1719 let home = "/home/session";
1721 assert_eq!(expand_tilde("~", Some(home)), home);
1722 assert_eq!(expand_tilde("~/foo", Some(home)), format!("{}/foo", home));
1723 assert_eq!(
1724 expand_tilde("~/foo/bar", Some(home)),
1725 format!("{}/foo/bar", home)
1726 );
1727 }
1728
1729 #[test]
1730 fn expand_tilde_hermetic_no_home_does_not_leak_host() {
1731 assert_eq!(expand_tilde("~", None), "~");
1734 assert_eq!(expand_tilde("~/foo", None), "~/foo");
1735 }
1736
1737 #[test]
1738 fn expand_tilde_passthrough() {
1739 assert_eq!(expand_tilde("/home/user", Some("/h")), "/home/user");
1741 assert_eq!(expand_tilde("foo~bar", Some("/h")), "foo~bar");
1742 assert_eq!(expand_tilde("", Some("/h")), "");
1743 }
1744
1745 #[test]
1746 #[cfg(all(unix, feature = "host"))]
1747 fn expand_tilde_user() {
1748 let expanded = expand_tilde("~root", None);
1751 assert!(
1753 expanded == "/root" || expanded == "/var/root",
1754 "expected /root or /var/root, got: {}",
1755 expanded
1756 );
1757
1758 let expanded_path = expand_tilde("~root/subdir", None);
1760 assert!(
1761 expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1762 "expected /root/subdir or /var/root/subdir, got: {}",
1763 expanded_path
1764 );
1765
1766 let nonexistent = expand_tilde("~nonexistent_user_12345", None);
1768 assert_eq!(nonexistent, "~nonexistent_user_12345");
1769 }
1770
1771 #[test]
1772 fn value_to_string_with_tilde_expansion() {
1773 let val = Value::String("~/test".into());
1775 assert_eq!(
1776 value_to_string_with_tilde(&val, Some("/home/session")),
1777 "/home/session/test"
1778 );
1779 }
1780
1781 #[test]
1782 fn eval_positional_param() {
1783 let mut scope = Scope::new();
1784 scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1785
1786 let expr = Expr::Positional(0);
1788 let result = eval_expr(&expr, &mut scope).unwrap();
1789 assert_eq!(result, Value::String("my_tool".into()));
1790
1791 let expr = Expr::Positional(1);
1793 let result = eval_expr(&expr, &mut scope).unwrap();
1794 assert_eq!(result, Value::String("hello".into()));
1795
1796 let expr = Expr::Positional(2);
1798 let result = eval_expr(&expr, &mut scope).unwrap();
1799 assert_eq!(result, Value::String("world".into()));
1800
1801 let expr = Expr::Positional(3);
1803 let result = eval_expr(&expr, &mut scope).unwrap();
1804 assert_eq!(result, Value::String("".into()));
1805 }
1806
1807 #[test]
1808 fn eval_all_args() {
1809 let mut scope = Scope::new();
1810 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1811
1812 let expr = Expr::AllArgs;
1813 let result = eval_expr(&expr, &mut scope).unwrap();
1814
1815 assert_eq!(result, Value::String("a b c".into()));
1817 }
1818
1819 #[test]
1820 fn eval_arg_count() {
1821 let mut scope = Scope::new();
1822 scope.set_positional("test", vec!["x".into(), "y".into()]);
1823
1824 let expr = Expr::ArgCount;
1825 let result = eval_expr(&expr, &mut scope).unwrap();
1826 assert_eq!(result, Value::Int(2));
1827 }
1828
1829 #[test]
1830 fn eval_arg_count_empty() {
1831 let mut scope = Scope::new();
1832
1833 let expr = Expr::ArgCount;
1834 let result = eval_expr(&expr, &mut scope).unwrap();
1835 assert_eq!(result, Value::Int(0));
1836 }
1837
1838 #[test]
1839 fn eval_var_length_string() {
1840 let mut scope = Scope::new();
1841 scope.set("NAME", Value::String("hello".into()));
1842
1843 let expr = Expr::VarLength(VarPath::simple("NAME"));
1844 let result = eval_expr(&expr, &mut scope).unwrap();
1845 assert_eq!(result, Value::Int(5));
1846 }
1847
1848 #[test]
1849 fn eval_var_length_empty_string() {
1850 let mut scope = Scope::new();
1851 scope.set("EMPTY", Value::String("".into()));
1852
1853 let expr = Expr::VarLength(VarPath::simple("EMPTY"));
1854 let result = eval_expr(&expr, &mut scope).unwrap();
1855 assert_eq!(result, Value::Int(0));
1856 }
1857
1858 #[test]
1859 fn eval_var_length_unset() {
1860 let mut scope = Scope::new();
1861
1862 let expr = Expr::VarLength(VarPath::simple("MISSING"));
1864 let result = eval_expr(&expr, &mut scope).unwrap();
1865 assert_eq!(result, Value::Int(0));
1866 }
1867
1868 #[test]
1869 fn eval_var_length_int() {
1870 let mut scope = Scope::new();
1871 scope.set("NUM", Value::Int(12345));
1872
1873 let expr = Expr::VarLength(VarPath::simple("NUM"));
1875 let result = eval_expr(&expr, &mut scope).unwrap();
1876 assert_eq!(result, Value::Int(5)); }
1878
1879 #[test]
1880 fn eval_var_with_default_set() {
1881 let mut scope = Scope::new();
1882 scope.set("NAME", Value::String("Alice".into()));
1883
1884 let expr = Expr::VarWithDefault {
1886 path: VarPath::simple("NAME"),
1887 default: vec![StringPart::Literal("default".into())],
1888 };
1889 let result = eval_expr(&expr, &mut scope).unwrap();
1890 assert_eq!(result, Value::String("Alice".into()));
1891 }
1892
1893 #[test]
1894 fn eval_var_with_default_unset() {
1895 let mut scope = Scope::new();
1896
1897 let expr = Expr::VarWithDefault {
1899 path: VarPath::simple("MISSING"),
1900 default: vec![StringPart::Literal("fallback".into())],
1901 };
1902 let result = eval_expr(&expr, &mut scope).unwrap();
1903 assert_eq!(result, Value::String("fallback".into()));
1904 }
1905
1906 #[test]
1907 fn eval_var_with_default_empty() {
1908 let mut scope = Scope::new();
1909 scope.set("EMPTY", Value::String("".into()));
1910
1911 let expr = Expr::VarWithDefault {
1913 path: VarPath::simple("EMPTY"),
1914 default: vec![StringPart::Literal("not empty".into())],
1915 };
1916 let result = eval_expr(&expr, &mut scope).unwrap();
1917 assert_eq!(result, Value::String("not empty".into()));
1918 }
1919
1920 #[test]
1921 fn eval_var_with_default_non_string() {
1922 let mut scope = Scope::new();
1923 scope.set("NUM", Value::Int(42));
1924
1925 let expr = Expr::VarWithDefault {
1927 path: VarPath::simple("NUM"),
1928 default: vec![StringPart::Literal("default".into())],
1929 };
1930 let result = eval_expr(&expr, &mut scope).unwrap();
1931 assert_eq!(result, Value::Int(42));
1932 }
1933
1934 #[test]
1935 fn eval_unset_variable_is_empty() {
1936 let mut scope = Scope::new();
1937 let parts = vec![
1938 StringPart::Literal("prefix:".into()),
1939 StringPart::Var(VarPath::simple("UNSET")),
1940 StringPart::Literal(":suffix".into()),
1941 ];
1942 let expr = Expr::Interpolated(parts);
1943 let result = eval_expr(&expr, &mut scope).unwrap();
1944 assert_eq!(result, Value::String("prefix::suffix".into()));
1945 }
1946
1947 #[test]
1948 fn eval_unset_variable_multiple() {
1949 let mut scope = Scope::new();
1950 scope.set("SET", Value::String("hello".into()));
1951 let parts = vec![
1952 StringPart::Var(VarPath::simple("UNSET1")),
1953 StringPart::Literal("-".into()),
1954 StringPart::Var(VarPath::simple("SET")),
1955 StringPart::Literal("-".into()),
1956 StringPart::Var(VarPath::simple("UNSET2")),
1957 ];
1958 let expr = Expr::Interpolated(parts);
1959 let result = eval_expr(&expr, &mut scope).unwrap();
1960 assert_eq!(result, Value::String("-hello-".into()));
1961 }
1962
1963 #[test]
1966 fn values_equal_scalars_still_work() {
1967 assert_eq!(
1968 values_equal(&Value::String("x".into()), &Value::String("x".into())),
1969 Ok(true)
1970 );
1971 assert_eq!(
1973 values_equal(&Value::String("42".into()), &Value::Int(42)),
1974 Ok(true)
1975 );
1976 }
1977
1978 #[test]
1979 fn values_equal_collection_vs_scalar_is_loud() {
1980 let list = Value::Json(serde_json::json!(["a", "b"]));
1981 let record = Value::Json(serde_json::json!({"k": 1}));
1982 assert!(
1983 matches!(values_equal(&list, &Value::String("banana".into())), Err(EvalError::Unsupported(_))),
1984 "list vs scalar must be a loud error, never silently false"
1985 );
1986 assert!(matches!(
1988 values_equal(&Value::String("x".into()), &record),
1989 Err(EvalError::Unsupported(_))
1990 ));
1991 }
1992
1993 #[test]
1994 fn values_equal_collection_vs_collection_is_structural() {
1995 let a = Value::Json(serde_json::json!({"a": 1, "b": 2}));
1997 let b = Value::Json(serde_json::json!({"b": 2, "a": 1}));
1998 assert_eq!(values_equal(&a, &b), Ok(true));
1999 }
2000
2001 #[test]
2004 fn values_equal_bytes_vs_bytes_still_works() {
2005 assert_eq!(
2007 values_equal(&Value::Bytes(vec![1, 2, 3]), &Value::Bytes(vec![1, 2, 3])),
2008 Ok(true)
2009 );
2010 assert_eq!(
2011 values_equal(&Value::Bytes(vec![1, 2, 3]), &Value::Bytes(vec![1, 2, 4])),
2012 Ok(false)
2013 );
2014 }
2015
2016 #[test]
2017 fn values_equal_bytes_vs_scalar_is_loud() {
2018 let bin = Value::Bytes(vec![0xff, 0x00]);
2022 assert!(matches!(
2023 values_equal(&bin, &Value::String("x".into())),
2024 Err(EvalError::Unsupported(_))
2025 ));
2026 assert!(matches!(
2027 values_equal(&Value::Int(1), &bin),
2028 Err(EvalError::Unsupported(_))
2029 ));
2030 }
2031
2032 #[test]
2033 fn eval_membership_bytes_needle_against_record_key_is_loud() {
2034 let record = Value::Json(serde_json::json!({"k": 1}));
2035 let bin = Value::Bytes(vec![0xff, 0x00]);
2036 assert!(matches!(
2037 eval_membership(&bin, &record),
2038 Err(EvalError::Unsupported(_))
2039 ));
2040 }
2041
2042 #[test]
2043 fn eval_membership_bytes_needle_against_list_is_not_a_match_not_an_abort() {
2044 let list = Value::Json(serde_json::json!(["a", "b"]));
2048 let bin = Value::Bytes(vec![0xff, 0x00]);
2049 assert_eq!(eval_membership(&bin, &list), Ok(false));
2050 }
2051
2052 #[test]
2053 fn value_length_of_bytes_is_byte_count() {
2054 assert_eq!(value_length(&Value::Bytes(vec![1, 2, 3])), 3);
2055 }
2056
2057 #[test]
2058 fn structured_export_error_flags_collections_passes_scalars() {
2059 let scalars = vec![
2061 ("A".to_string(), Value::String("x".into())),
2062 ("B".to_string(), Value::Int(1)),
2063 ];
2064 assert!(structured_export_error(&scalars).is_none());
2065 let with_record = vec![(
2067 "CFG".to_string(),
2068 Value::Json(serde_json::json!({"port": 8080})),
2069 )];
2070 let msg = structured_export_error(&with_record).expect("record must be refused");
2071 assert!(msg.contains("CFG") && msg.contains("tojson"), "got: {msg}");
2072 let with_list = vec![("XS".to_string(), Value::Json(serde_json::json!([1, 2])))];
2074 assert!(structured_export_error(&with_list).is_some());
2075 }
2076
2077 #[test]
2078 fn defaults_on_emptiness_matches_decision_a() {
2079 assert!(value_defaults_on_emptiness(&Value::Null));
2082 assert!(value_defaults_on_emptiness(&Value::Json(serde_json::Value::Null)));
2083 assert!(value_defaults_on_emptiness(&Value::String(String::new())));
2084 assert!(!value_defaults_on_emptiness(&Value::Bool(false)));
2085 assert!(!value_defaults_on_emptiness(&Value::Int(0)));
2086 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!([]))));
2087 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!({}))));
2088 assert!(!value_defaults_on_emptiness(&Value::String("x".into())));
2089 }
2090
2091 #[test]
2092 fn subscripted_length_and_default_resolve_the_path() {
2093 let mut scope = Scope::new();
2096 scope.set("u", Value::Json(serde_json::json!({"tags": ["a", "b"]})));
2097 let len = eval_expr(
2098 &Expr::VarLength(crate::parser::parse_varpath("${u[tags]}")),
2099 &mut scope,
2100 )
2101 .unwrap();
2102 assert_eq!(len, Value::Int(2));
2103
2104 scope.set("cfg", Value::Json(serde_json::json!({"port": 9000})));
2105 let val = eval_expr(
2107 &Expr::VarWithDefault {
2108 path: crate::parser::parse_varpath("${cfg[port]}"),
2109 default: vec![StringPart::Literal("8080".into())],
2110 },
2111 &mut scope,
2112 )
2113 .unwrap();
2114 assert_eq!(value_to_string(&val), "9000");
2115
2116 let missing = eval_expr(
2118 &Expr::VarWithDefault {
2119 path: crate::parser::parse_varpath("${cfg[nope]}"),
2120 default: vec![StringPart::Literal("8080".into())],
2121 },
2122 &mut scope,
2123 )
2124 .unwrap();
2125 assert_eq!(value_to_string(&missing), "8080");
2126
2127 let err = eval_expr(
2129 &Expr::VarWithDefault {
2130 path: crate::parser::parse_varpath("${cfg[0]}"),
2131 default: vec![StringPart::Literal("x".into())],
2132 },
2133 &mut scope,
2134 )
2135 .unwrap_err();
2136 assert!(matches!(err, EvalError::InvalidPath(_)), "got: {err}");
2137 }
2138}