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> {
800 value_to_text_sink_named(value, "text")
801}
802
803pub fn value_to_text_sink_named(value: &Value, sink: &str) -> EvalResult<String> {
812 match value {
813 Value::Bytes(b) => match std::str::from_utf8(b) {
814 Ok(s) => Ok(s.to_string()),
815 Err(_) => Err(EvalError::Unsupported(format!(
816 "binary data ({} bytes) cannot be used as {sink} — decode it \
817 (base64/xxd) or redirect to a file",
818 b.len()
819 ))),
820 },
821 other => Ok(value_to_string(other)),
822 }
823}
824
825pub fn values_to_text_sink_named(values: &[Value], sink: &str) -> EvalResult<Vec<String>> {
829 values.iter().map(|v| value_to_text_sink_named(v, sink)).collect()
830}
831
832pub fn value_to_bool(value: &Value) -> bool {
842 match value {
843 Value::Null => false,
844 Value::Bool(b) => *b,
845 Value::Int(i) => *i != 0,
846 Value::Float(f) => *f != 0.0,
847 Value::String(s) => !s.is_empty(),
848 Value::Json(json) => match json {
849 serde_json::Value::Null => false,
850 serde_json::Value::Array(arr) => !arr.is_empty(),
851 serde_json::Value::Object(obj) => !obj.is_empty(),
852 serde_json::Value::Bool(b) => *b,
853 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
854 serde_json::Value::String(s) => !s.is_empty(),
855 },
856 Value::Bytes(b) => !b.is_empty(), }
858}
859
860pub fn expand_tilde(s: &str, home: Option<&str>) -> String {
874 if s == "~" {
875 home.map(|h| h.to_string()).unwrap_or_else(|| "~".to_string())
876 } else if s.starts_with("~/") {
877 match home {
878 Some(home) => format!("{}{}", home, &s[1..]),
879 None => s.to_string(),
880 }
881 } else if s.starts_with('~') {
882 expand_tilde_user(s)
884 } else {
885 s.to_string()
886 }
887}
888
889#[cfg(all(unix, feature = "host"))]
894fn expand_tilde_user(s: &str) -> String {
895 let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
897 (&s[1..slash_pos + 1], &s[slash_pos + 1..])
898 } else {
899 (&s[1..], "")
900 };
901
902 if username.is_empty() {
903 return s.to_string();
904 }
905
906 let passwd = match std::fs::read_to_string("/etc/passwd") {
909 Ok(content) => content,
910 Err(_) => return s.to_string(),
911 };
912
913 for line in passwd.lines() {
914 let fields: Vec<&str> = line.split(':').collect();
915 if fields.len() >= 6 && fields[0] == username {
916 let home_dir = fields[5];
917 return if rest.is_empty() {
918 home_dir.to_string()
919 } else {
920 format!("{}{}", home_dir, rest)
921 };
922 }
923 }
924
925 s.to_string()
927}
928
929#[cfg(not(all(unix, feature = "host")))]
930fn expand_tilde_user(s: &str) -> String {
931 s.to_string()
934}
935
936pub fn value_to_string_with_tilde(value: &Value, home: Option<&str>) -> String {
941 match value {
942 Value::String(s) if s.starts_with('~') => expand_tilde(s, home),
943 _ => value_to_string(value),
944 }
945}
946
947pub(crate) fn format_path(path: &VarPath) -> String {
952 use crate::ast::VarSegment;
953 let mut result = String::from("${");
954 for (i, seg) in path.segments.iter().enumerate() {
955 match seg {
956 VarSegment::Field(name) => {
957 if i > 0 {
958 result.push('.');
959 }
960 result.push_str(name);
961 }
962 VarSegment::Index(idx) => result.push_str(&format!("[{idx}]")),
963 VarSegment::Key(k) => result.push_str(&format!("[{k}]")),
964 VarSegment::Dynamic(v) => result.push_str(&format!("[${v}]")),
965 VarSegment::Slice(a, b) => {
966 let s = a.map(|n| n.to_string()).unwrap_or_default();
967 let e = b.map(|n| n.to_string()).unwrap_or_default();
968 result.push_str(&format!("[{s}:{e}]"));
969 }
970 }
971 }
972 result.push('}');
973 result
974}
975
976fn is_truthy(value: &Value) -> bool {
986 value_to_bool(value)
988}
989
990pub fn values_equal(left: &Value, right: &Value) -> EvalResult<bool> {
1001 match (left, right) {
1002 (Value::Null, Value::Null) => Ok(true),
1003 (Value::Bool(a), Value::Bool(b)) => Ok(a == b),
1004 (Value::Int(a), Value::Int(b)) => Ok(a == b),
1005 (Value::Float(a), Value::Float(b)) => Ok((a - b).abs() < f64::EPSILON),
1006 (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
1007 Ok((*a as f64 - b).abs() < f64::EPSILON)
1008 }
1009 (Value::String(a), Value::String(b)) => Ok(a == b),
1010 (Value::Json(a), Value::Json(b)) => Ok(a == b),
1011 (Value::Bytes(a), Value::Bytes(b)) => Ok(a == b),
1012 (Value::Json(j), other) | (other, Value::Json(j))
1018 if matches!(j, serde_json::Value::Array(_) | serde_json::Value::Object(_)) =>
1019 {
1020 let kind = if j.is_array() { "list" } else { "record" };
1021 Err(EvalError::Unsupported(format!(
1022 "cannot compare a {kind} to a {other_kind} with ==/!= — test membership with `[[ x in $coll ]]`, or compare structures with `jq`",
1023 other_kind = type_name(other),
1024 )))
1025 }
1026 (Value::Bytes(b), other) | (other, Value::Bytes(b)) => Err(EvalError::Unsupported(format!(
1032 "binary data ({} bytes) cannot be used as an ==/!= operand against a {} — decode it \
1033 first (base64/xxd), or compare two binary values directly",
1034 b.len(),
1035 type_name(other),
1036 ))),
1037 _ => Ok(value_to_string(left) == value_to_string(right)),
1040 }
1041}
1042
1043fn element_matches(needle: &Value, element: &Value) -> bool {
1052 match (needle, element) {
1053 (Value::Json(a), Value::Json(b)) => a == b,
1054 (Value::Json(_), _) | (_, Value::Json(_)) => false,
1055 _ => values_equal(needle, element).unwrap_or(false),
1062 }
1063}
1064
1065fn eval_membership(needle: &Value, haystack: &Value) -> EvalResult<bool> {
1075 match haystack {
1076 Value::Json(serde_json::Value::Array(items)) => {
1077 for item in items {
1078 let element = json_to_value_no_envelope(item.clone());
1079 if element_matches(needle, &element) {
1080 return Ok(true);
1081 }
1082 }
1083 Ok(false)
1084 }
1085 Value::Json(serde_json::Value::Object(map)) => {
1086 if let Value::Bytes(b) = needle {
1091 return Err(EvalError::Unsupported(format!(
1092 "binary data ({} bytes) cannot be used as a record key for `in` — \
1093 decode it first (base64/xxd)",
1094 b.len()
1095 )));
1096 }
1097 Ok(map.contains_key(&value_to_string(needle)))
1098 }
1099 other => Err(EvalError::Unsupported(format!(
1100 "`in` requires a list or record on the right-hand side, got {} — substring tests use `=~`, glob (`[[ $s == *sub* ]]`), or `case`",
1101 type_name(other),
1102 ))),
1103 }
1104}
1105
1106fn cmp_op_symbol(op: &TestCmpOp) -> &'static str {
1109 match op {
1110 TestCmpOp::Eq => "==",
1111 TestCmpOp::NotEq => "!=",
1112 TestCmpOp::Match => "=~",
1113 TestCmpOp::NotMatch => "!~",
1114 TestCmpOp::Gt => ">",
1115 TestCmpOp::Lt => "<",
1116 TestCmpOp::GtEq => ">=",
1117 TestCmpOp::LtEq => "<=",
1118 TestCmpOp::NumEq => "-eq",
1119 TestCmpOp::NumNotEq => "-ne",
1120 TestCmpOp::NumGt => "-gt",
1121 TestCmpOp::NumLt => "-lt",
1122 TestCmpOp::NumGtEq => "-ge",
1123 TestCmpOp::NumLtEq => "-le",
1124 }
1125}
1126
1127fn guard_scalar_test_operands(op: &TestCmpOp, left: &Value, right: &Value) -> EvalResult<()> {
1131 let symbol = cmp_op_symbol(op);
1132 if let Some(msg) = scalar_test_operand_error(symbol, left) {
1133 return Err(EvalError::Unsupported(msg));
1134 }
1135 if let Some(msg) = scalar_test_operand_error(symbol, right) {
1136 return Err(EvalError::Unsupported(msg));
1137 }
1138 Ok(())
1139}
1140
1141fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1143 match (left, right) {
1144 (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
1145 (Value::Float(a), Value::Float(b)) => {
1146 a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1147 }
1148 (Value::Int(a), Value::Float(b)) => {
1149 (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1150 }
1151 (Value::Float(a), Value::Int(b)) => {
1152 a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1153 }
1154 (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
1155 _ => Err(EvalError::TypeError {
1156 expected: "comparable types (numbers or strings)",
1157 got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
1158 }),
1159 }
1160}
1161
1162enum Num {
1167 Int(i64),
1168 Float(f64),
1169}
1170
1171fn value_to_num(value: &Value) -> EvalResult<Num> {
1172 match value {
1173 Value::Int(n) => Ok(Num::Int(*n)),
1174 Value::Float(f) => Ok(Num::Float(*f)),
1175 Value::String(s) => {
1176 let t = s.trim();
1177 if let Ok(n) = t.parse::<i64>() {
1178 Ok(Num::Int(n))
1179 } else if let Ok(f) = t.parse::<f64>() {
1180 Ok(Num::Float(f))
1181 } else {
1182 Err(EvalError::TypeError {
1183 expected: "numeric operand",
1184 got: format!("non-numeric string {:?}", s),
1185 })
1186 }
1187 }
1188 _ => Err(EvalError::TypeError {
1189 expected: "numeric operand",
1190 got: type_name(value).to_string(),
1191 }),
1192 }
1193}
1194
1195pub fn numeric_compare(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1200 let l = value_to_num(left)?;
1201 let r = value_to_num(right)?;
1202 match (l, r) {
1203 (Num::Int(a), Num::Int(b)) => Ok(a.cmp(&b)),
1204 (Num::Float(a), Num::Float(b)) => a
1205 .partial_cmp(&b)
1206 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1207 (Num::Int(a), Num::Float(b)) => (a as f64)
1208 .partial_cmp(&b)
1209 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1210 (Num::Float(a), Num::Int(b)) => a
1211 .partial_cmp(&(b as f64))
1212 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1213 }
1214}
1215
1216fn type_name(value: &Value) -> &'static str {
1218 match value {
1219 Value::Null => "null",
1220 Value::Bool(_) => "bool",
1221 Value::Int(_) => "int",
1222 Value::Float(_) => "float",
1223 Value::String(_) => "string",
1224 Value::Json(_) => "json",
1225 Value::Bytes(_) => "bytes",
1226 }
1227}
1228
1229fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
1234 let text = match left {
1235 Value::String(s) => s.as_str(),
1236 _ => {
1237 return Err(EvalError::TypeError {
1238 expected: "string",
1239 got: type_name(left).to_string(),
1240 })
1241 }
1242 };
1243
1244 let pattern = match right {
1245 Value::String(s) => s.as_str(),
1246 _ => {
1247 return Err(EvalError::TypeError {
1248 expected: "string (regex pattern)",
1249 got: type_name(right).to_string(),
1250 })
1251 }
1252 };
1253
1254 let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
1255 let matches = re.is_match(text);
1256
1257 Ok(Value::Bool(if negate { !matches } else { matches }))
1258}
1259
1260pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
1267 let mut evaluator = Evaluator::new(scope);
1268 evaluator.eval(expr)
1269}
1270
1271#[cfg(test)]
1272#[allow(clippy::approx_constant)]
1273mod tests {
1274 use super::*;
1275 use crate::ast::{Stmt, VarSegment};
1276 use super::super::result::ExecResult;
1277
1278 fn var_expr(name: &str) -> Expr {
1280 Expr::VarRef(VarPath::simple(name))
1281 }
1282
1283 #[test]
1284 fn eval_literal_int() {
1285 let mut scope = Scope::new();
1286 let expr = Expr::Literal(Value::Int(42));
1287 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1288 }
1289
1290 #[test]
1291 fn eval_literal_string() {
1292 let mut scope = Scope::new();
1293 let expr = Expr::Literal(Value::String("hello".into()));
1294 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
1295 }
1296
1297 #[test]
1298 fn eval_literal_bool() {
1299 let mut scope = Scope::new();
1300 assert_eq!(
1301 eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
1302 Ok(Value::Bool(true))
1303 );
1304 }
1305
1306 #[test]
1307 fn eval_literal_null() {
1308 let mut scope = Scope::new();
1309 assert_eq!(
1310 eval_expr(&Expr::Literal(Value::Null), &mut scope),
1311 Ok(Value::Null)
1312 );
1313 }
1314
1315 #[test]
1316 fn eval_literal_float() {
1317 let mut scope = Scope::new();
1318 let expr = Expr::Literal(Value::Float(3.14));
1319 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
1320 }
1321
1322 #[test]
1323 fn eval_variable_ref() {
1324 let mut scope = Scope::new();
1325 scope.set("X", Value::Int(100));
1326 assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
1327 }
1328
1329 #[test]
1330 fn eval_undefined_variable() {
1331 let mut scope = Scope::new();
1332 let result = eval_expr(&var_expr("MISSING"), &mut scope);
1333 assert!(matches!(result, Err(EvalError::InvalidPath(_))));
1334 }
1335
1336 #[test]
1337 fn eval_interpolated_string() {
1338 let mut scope = Scope::new();
1339 scope.set("NAME", Value::String("World".into()));
1340
1341 let expr = Expr::Interpolated(vec![
1342 StringPart::Literal("Hello, ".into()),
1343 StringPart::Var(VarPath::simple("NAME")),
1344 StringPart::Literal("!".into()),
1345 ]);
1346 assert_eq!(
1347 eval_expr(&expr, &mut scope),
1348 Ok(Value::String("Hello, World!".into()))
1349 );
1350 }
1351
1352 #[test]
1364 fn eval_heredoc_body_binary_var_is_loud() {
1365 let mut scope = Scope::new();
1366 scope.set("B", Value::Bytes(vec![0xff, 0x00, 0xfe]));
1367
1368 let expr = Expr::HereDocBody {
1369 parts: vec![
1370 crate::ast::SpannedPart {
1371 part: StringPart::Literal("before ".into()),
1372 offset: 0,
1373 len: 0,
1374 },
1375 crate::ast::SpannedPart {
1376 part: StringPart::Var(VarPath::simple("B")),
1377 offset: 0,
1378 len: 0,
1379 },
1380 ],
1381 strip_tabs: false,
1382 };
1383 let err = eval_expr(&expr, &mut scope).expect_err("binary in a heredoc body must be loud");
1384 assert!(
1385 matches!(err, EvalError::Unsupported(ref msg) if msg.contains("cannot be used as")),
1386 "got {err:?}"
1387 );
1388 }
1389
1390 #[test]
1391 fn eval_heredoc_body_text_var_is_unaffected() {
1392 let mut scope = Scope::new();
1393 scope.set("NAME", Value::String("World".into()));
1394
1395 let expr = Expr::HereDocBody {
1396 parts: vec![
1397 crate::ast::SpannedPart {
1398 part: StringPart::Literal("Hello, ".into()),
1399 offset: 0,
1400 len: 0,
1401 },
1402 crate::ast::SpannedPart {
1403 part: StringPart::Var(VarPath::simple("NAME")),
1404 offset: 0,
1405 len: 0,
1406 },
1407 ],
1408 strip_tabs: false,
1409 };
1410 assert_eq!(
1411 eval_expr(&expr, &mut scope),
1412 Ok(Value::String("Hello, World".into()))
1413 );
1414 }
1415
1416 #[test]
1417 fn eval_record_literal_interpolated_key_binary_var_is_loud() {
1418 let mut scope = Scope::new();
1419 scope.set("B", Value::Bytes(vec![0xff, 0x00, 0xfe]));
1420
1421 let expr = Expr::RecordLiteral(vec![RecordEntry {
1422 key: RecordKey::Interpolated(vec![StringPart::Var(VarPath::simple("B"))]),
1423 value: Expr::Literal(Value::Int(1)),
1424 }]);
1425 let err = eval_expr(&expr, &mut scope)
1426 .expect_err("a binary record key must be loud, not a `[binary: N bytes]` key");
1427 assert!(
1428 matches!(err, EvalError::Unsupported(ref msg) if msg.contains("cannot be used as")),
1429 "got {err:?}"
1430 );
1431 }
1432
1433 #[test]
1434 fn eval_record_literal_interpolated_key_text_var_is_unaffected() {
1435 let mut scope = Scope::new();
1436 scope.set("K", Value::String("port".into()));
1437
1438 let expr = Expr::RecordLiteral(vec![RecordEntry {
1439 key: RecordKey::Interpolated(vec![StringPart::Var(VarPath::simple("K"))]),
1440 value: Expr::Literal(Value::Int(8080)),
1441 }]);
1442 assert_eq!(
1443 eval_expr(&expr, &mut scope),
1444 Ok(Value::Json(serde_json::json!({"port": 8080})))
1445 );
1446 }
1447
1448 #[test]
1449 fn eval_interpolated_with_number() {
1450 let mut scope = Scope::new();
1451 scope.set("COUNT", Value::Int(42));
1452
1453 let expr = Expr::Interpolated(vec![
1454 StringPart::Literal("Count: ".into()),
1455 StringPart::Var(VarPath::simple("COUNT")),
1456 ]);
1457 assert_eq!(
1458 eval_expr(&expr, &mut scope),
1459 Ok(Value::String("Count: 42".into()))
1460 );
1461 }
1462
1463 #[test]
1464 fn eval_and_short_circuit_true() {
1465 let mut scope = Scope::new();
1466 let expr = Expr::BinaryOp {
1467 left: Box::new(Expr::Literal(Value::Bool(true))),
1468 op: BinaryOp::And,
1469 right: Box::new(Expr::Literal(Value::Int(42))),
1470 };
1471 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1473 }
1474
1475 #[test]
1476 fn eval_and_short_circuit_false() {
1477 let mut scope = Scope::new();
1478 let expr = Expr::BinaryOp {
1479 left: Box::new(Expr::Literal(Value::Bool(false))),
1480 op: BinaryOp::And,
1481 right: Box::new(Expr::Literal(Value::Int(42))),
1482 };
1483 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
1485 }
1486
1487 #[test]
1488 fn eval_or_short_circuit_true() {
1489 let mut scope = Scope::new();
1490 let expr = Expr::BinaryOp {
1491 left: Box::new(Expr::Literal(Value::Bool(true))),
1492 op: BinaryOp::Or,
1493 right: Box::new(Expr::Literal(Value::Int(42))),
1494 };
1495 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1497 }
1498
1499 #[test]
1500 fn eval_or_short_circuit_false() {
1501 let mut scope = Scope::new();
1502 let expr = Expr::BinaryOp {
1503 left: Box::new(Expr::Literal(Value::Bool(false))),
1504 op: BinaryOp::Or,
1505 right: Box::new(Expr::Literal(Value::Int(42))),
1506 };
1507 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1509 }
1510
1511 #[test]
1512 fn is_truthy_values() {
1513 assert!(!is_truthy(&Value::Null));
1514 assert!(!is_truthy(&Value::Bool(false)));
1515 assert!(is_truthy(&Value::Bool(true)));
1516 assert!(!is_truthy(&Value::Int(0)));
1517 assert!(is_truthy(&Value::Int(1)));
1518 assert!(is_truthy(&Value::Int(-1)));
1519 assert!(!is_truthy(&Value::Float(0.0)));
1520 assert!(is_truthy(&Value::Float(0.1)));
1521 assert!(!is_truthy(&Value::String("".into())));
1522 assert!(is_truthy(&Value::String("x".into())));
1523 }
1524
1525 #[test]
1526 fn sync_command_subst_is_loud_not_silent() {
1527 use crate::ast::Command;
1531
1532 let mut scope = Scope::new();
1533 let expr = Expr::CommandSubst(vec![Stmt::Command(Command {
1534 name: "echo".into(),
1535 args: vec![],
1536 redirects: vec![],
1537 })]);
1538
1539 assert!(matches!(
1540 eval_expr(&expr, &mut scope),
1541 Err(EvalError::NoExecutor)
1542 ));
1543 }
1544
1545 #[test]
1546 fn eval_last_result_bare() {
1547 let mut scope = Scope::new();
1550 scope.set_last_result(ExecResult::failure(42, "test error"));
1551
1552 let expr = Expr::VarRef(VarPath {
1553 segments: vec![VarSegment::Field("?".into())],
1554 });
1555 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1556 }
1557
1558 #[test]
1559 fn value_to_string_all_types() {
1560 assert_eq!(value_to_string(&Value::Null), "null");
1561 assert_eq!(value_to_string(&Value::Bool(true)), "true");
1562 assert_eq!(value_to_string(&Value::Int(42)), "42");
1563 assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1564 assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1565 }
1566
1567 #[test]
1570 fn eval_negative_int() {
1571 let mut scope = Scope::new();
1572 let expr = Expr::Literal(Value::Int(-42));
1573 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1574 }
1575
1576 #[test]
1577 fn eval_negative_float() {
1578 let mut scope = Scope::new();
1579 let expr = Expr::Literal(Value::Float(-3.14));
1580 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1581 }
1582
1583 #[test]
1584 fn eval_zero_values() {
1585 let mut scope = Scope::new();
1586 assert_eq!(
1587 eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1588 Ok(Value::Int(0))
1589 );
1590 assert_eq!(
1591 eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1592 Ok(Value::Float(0.0))
1593 );
1594 }
1595
1596 #[test]
1597 fn eval_interpolation_empty_var() {
1598 let mut scope = Scope::new();
1599 scope.set("EMPTY", Value::String("".into()));
1600
1601 let expr = Expr::Interpolated(vec![
1602 StringPart::Literal("prefix".into()),
1603 StringPart::Var(VarPath::simple("EMPTY")),
1604 StringPart::Literal("suffix".into()),
1605 ]);
1606 assert_eq!(
1607 eval_expr(&expr, &mut scope),
1608 Ok(Value::String("prefixsuffix".into()))
1609 );
1610 }
1611
1612 #[test]
1613 fn eval_chained_and() {
1614 let mut scope = Scope::new();
1615 let expr = Expr::BinaryOp {
1617 left: Box::new(Expr::BinaryOp {
1618 left: Box::new(Expr::Literal(Value::Bool(true))),
1619 op: BinaryOp::And,
1620 right: Box::new(Expr::Literal(Value::Bool(true))),
1621 }),
1622 op: BinaryOp::And,
1623 right: Box::new(Expr::Literal(Value::Int(42))),
1624 };
1625 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1626 }
1627
1628 #[test]
1629 fn eval_chained_or() {
1630 let mut scope = Scope::new();
1631 let expr = Expr::BinaryOp {
1633 left: Box::new(Expr::BinaryOp {
1634 left: Box::new(Expr::Literal(Value::Bool(false))),
1635 op: BinaryOp::Or,
1636 right: Box::new(Expr::Literal(Value::Bool(false))),
1637 }),
1638 op: BinaryOp::Or,
1639 right: Box::new(Expr::Literal(Value::Int(42))),
1640 };
1641 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1642 }
1643
1644 #[test]
1645 fn eval_mixed_and_or() {
1646 let mut scope = Scope::new();
1647 let expr = Expr::BinaryOp {
1650 left: Box::new(Expr::BinaryOp {
1651 left: Box::new(Expr::Literal(Value::Bool(true))),
1652 op: BinaryOp::Or,
1653 right: Box::new(Expr::Literal(Value::Bool(false))),
1654 }),
1655 op: BinaryOp::And,
1656 right: Box::new(Expr::Literal(Value::Bool(true))),
1657 };
1658 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1660 }
1661
1662 #[test]
1663 fn eval_interpolation_with_bool() {
1664 let mut scope = Scope::new();
1665 scope.set("FLAG", Value::Bool(true));
1666
1667 let expr = Expr::Interpolated(vec![
1668 StringPart::Literal("enabled: ".into()),
1669 StringPart::Var(VarPath::simple("FLAG")),
1670 ]);
1671 assert_eq!(
1672 eval_expr(&expr, &mut scope),
1673 Ok(Value::String("enabled: true".into()))
1674 );
1675 }
1676
1677 #[test]
1678 fn eval_interpolation_with_null() {
1679 let mut scope = Scope::new();
1680 scope.set("VAL", Value::Null);
1681
1682 let expr = Expr::Interpolated(vec![
1683 StringPart::Literal("value: ".into()),
1684 StringPart::Var(VarPath::simple("VAL")),
1685 ]);
1686 assert_eq!(
1687 eval_expr(&expr, &mut scope),
1688 Ok(Value::String("value: null".into()))
1689 );
1690 }
1691
1692 #[test]
1693 fn eval_format_path_simple() {
1694 let path = VarPath::simple("X");
1695 assert_eq!(format_path(&path), "${X}");
1696 }
1697
1698 #[test]
1699 fn eval_format_path_nested() {
1700 let path = VarPath {
1701 segments: vec![
1702 VarSegment::Field("X".into()),
1703 VarSegment::Field("field".into()),
1704 ],
1705 };
1706 assert_eq!(format_path(&path), "${X.field}");
1707 }
1708
1709 #[test]
1710 fn type_name_all_types() {
1711 assert_eq!(type_name(&Value::Null), "null");
1712 assert_eq!(type_name(&Value::Bool(true)), "bool");
1713 assert_eq!(type_name(&Value::Int(1)), "int");
1714 assert_eq!(type_name(&Value::Float(1.0)), "float");
1715 assert_eq!(type_name(&Value::String("".into())), "string");
1716 }
1717
1718 #[test]
1719 fn expand_tilde_home() {
1720 let home = "/home/session";
1722 assert_eq!(expand_tilde("~", Some(home)), home);
1723 assert_eq!(expand_tilde("~/foo", Some(home)), format!("{}/foo", home));
1724 assert_eq!(
1725 expand_tilde("~/foo/bar", Some(home)),
1726 format!("{}/foo/bar", home)
1727 );
1728 }
1729
1730 #[test]
1731 fn expand_tilde_hermetic_no_home_does_not_leak_host() {
1732 assert_eq!(expand_tilde("~", None), "~");
1735 assert_eq!(expand_tilde("~/foo", None), "~/foo");
1736 }
1737
1738 #[test]
1739 fn expand_tilde_passthrough() {
1740 assert_eq!(expand_tilde("/home/user", Some("/h")), "/home/user");
1742 assert_eq!(expand_tilde("foo~bar", Some("/h")), "foo~bar");
1743 assert_eq!(expand_tilde("", Some("/h")), "");
1744 }
1745
1746 #[test]
1747 #[cfg(all(unix, feature = "host"))]
1748 fn expand_tilde_user() {
1749 let expanded = expand_tilde("~root", None);
1752 assert!(
1754 expanded == "/root" || expanded == "/var/root",
1755 "expected /root or /var/root, got: {}",
1756 expanded
1757 );
1758
1759 let expanded_path = expand_tilde("~root/subdir", None);
1761 assert!(
1762 expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1763 "expected /root/subdir or /var/root/subdir, got: {}",
1764 expanded_path
1765 );
1766
1767 let nonexistent = expand_tilde("~nonexistent_user_12345", None);
1769 assert_eq!(nonexistent, "~nonexistent_user_12345");
1770 }
1771
1772 #[test]
1773 fn value_to_string_with_tilde_expansion() {
1774 let val = Value::String("~/test".into());
1776 assert_eq!(
1777 value_to_string_with_tilde(&val, Some("/home/session")),
1778 "/home/session/test"
1779 );
1780 }
1781
1782 #[test]
1783 fn eval_positional_param() {
1784 let mut scope = Scope::new();
1785 scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1786
1787 let expr = Expr::Positional(0);
1789 let result = eval_expr(&expr, &mut scope).unwrap();
1790 assert_eq!(result, Value::String("my_tool".into()));
1791
1792 let expr = Expr::Positional(1);
1794 let result = eval_expr(&expr, &mut scope).unwrap();
1795 assert_eq!(result, Value::String("hello".into()));
1796
1797 let expr = Expr::Positional(2);
1799 let result = eval_expr(&expr, &mut scope).unwrap();
1800 assert_eq!(result, Value::String("world".into()));
1801
1802 let expr = Expr::Positional(3);
1804 let result = eval_expr(&expr, &mut scope).unwrap();
1805 assert_eq!(result, Value::String("".into()));
1806 }
1807
1808 #[test]
1809 fn eval_all_args() {
1810 let mut scope = Scope::new();
1811 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1812
1813 let expr = Expr::AllArgs;
1814 let result = eval_expr(&expr, &mut scope).unwrap();
1815
1816 assert_eq!(result, Value::String("a b c".into()));
1818 }
1819
1820 #[test]
1821 fn eval_arg_count() {
1822 let mut scope = Scope::new();
1823 scope.set_positional("test", vec!["x".into(), "y".into()]);
1824
1825 let expr = Expr::ArgCount;
1826 let result = eval_expr(&expr, &mut scope).unwrap();
1827 assert_eq!(result, Value::Int(2));
1828 }
1829
1830 #[test]
1831 fn eval_arg_count_empty() {
1832 let mut scope = Scope::new();
1833
1834 let expr = Expr::ArgCount;
1835 let result = eval_expr(&expr, &mut scope).unwrap();
1836 assert_eq!(result, Value::Int(0));
1837 }
1838
1839 #[test]
1840 fn eval_var_length_string() {
1841 let mut scope = Scope::new();
1842 scope.set("NAME", Value::String("hello".into()));
1843
1844 let expr = Expr::VarLength(VarPath::simple("NAME"));
1845 let result = eval_expr(&expr, &mut scope).unwrap();
1846 assert_eq!(result, Value::Int(5));
1847 }
1848
1849 #[test]
1850 fn eval_var_length_empty_string() {
1851 let mut scope = Scope::new();
1852 scope.set("EMPTY", Value::String("".into()));
1853
1854 let expr = Expr::VarLength(VarPath::simple("EMPTY"));
1855 let result = eval_expr(&expr, &mut scope).unwrap();
1856 assert_eq!(result, Value::Int(0));
1857 }
1858
1859 #[test]
1860 fn eval_var_length_unset() {
1861 let mut scope = Scope::new();
1862
1863 let expr = Expr::VarLength(VarPath::simple("MISSING"));
1865 let result = eval_expr(&expr, &mut scope).unwrap();
1866 assert_eq!(result, Value::Int(0));
1867 }
1868
1869 #[test]
1870 fn eval_var_length_int() {
1871 let mut scope = Scope::new();
1872 scope.set("NUM", Value::Int(12345));
1873
1874 let expr = Expr::VarLength(VarPath::simple("NUM"));
1876 let result = eval_expr(&expr, &mut scope).unwrap();
1877 assert_eq!(result, Value::Int(5)); }
1879
1880 #[test]
1881 fn eval_var_with_default_set() {
1882 let mut scope = Scope::new();
1883 scope.set("NAME", Value::String("Alice".into()));
1884
1885 let expr = Expr::VarWithDefault {
1887 path: VarPath::simple("NAME"),
1888 default: vec![StringPart::Literal("default".into())],
1889 };
1890 let result = eval_expr(&expr, &mut scope).unwrap();
1891 assert_eq!(result, Value::String("Alice".into()));
1892 }
1893
1894 #[test]
1895 fn eval_var_with_default_unset() {
1896 let mut scope = Scope::new();
1897
1898 let expr = Expr::VarWithDefault {
1900 path: VarPath::simple("MISSING"),
1901 default: vec![StringPart::Literal("fallback".into())],
1902 };
1903 let result = eval_expr(&expr, &mut scope).unwrap();
1904 assert_eq!(result, Value::String("fallback".into()));
1905 }
1906
1907 #[test]
1908 fn eval_var_with_default_empty() {
1909 let mut scope = Scope::new();
1910 scope.set("EMPTY", Value::String("".into()));
1911
1912 let expr = Expr::VarWithDefault {
1914 path: VarPath::simple("EMPTY"),
1915 default: vec![StringPart::Literal("not empty".into())],
1916 };
1917 let result = eval_expr(&expr, &mut scope).unwrap();
1918 assert_eq!(result, Value::String("not empty".into()));
1919 }
1920
1921 #[test]
1922 fn eval_var_with_default_non_string() {
1923 let mut scope = Scope::new();
1924 scope.set("NUM", Value::Int(42));
1925
1926 let expr = Expr::VarWithDefault {
1928 path: VarPath::simple("NUM"),
1929 default: vec![StringPart::Literal("default".into())],
1930 };
1931 let result = eval_expr(&expr, &mut scope).unwrap();
1932 assert_eq!(result, Value::Int(42));
1933 }
1934
1935 #[test]
1936 fn eval_unset_variable_is_empty() {
1937 let mut scope = Scope::new();
1938 let parts = vec![
1939 StringPart::Literal("prefix:".into()),
1940 StringPart::Var(VarPath::simple("UNSET")),
1941 StringPart::Literal(":suffix".into()),
1942 ];
1943 let expr = Expr::Interpolated(parts);
1944 let result = eval_expr(&expr, &mut scope).unwrap();
1945 assert_eq!(result, Value::String("prefix::suffix".into()));
1946 }
1947
1948 #[test]
1949 fn eval_unset_variable_multiple() {
1950 let mut scope = Scope::new();
1951 scope.set("SET", Value::String("hello".into()));
1952 let parts = vec![
1953 StringPart::Var(VarPath::simple("UNSET1")),
1954 StringPart::Literal("-".into()),
1955 StringPart::Var(VarPath::simple("SET")),
1956 StringPart::Literal("-".into()),
1957 StringPart::Var(VarPath::simple("UNSET2")),
1958 ];
1959 let expr = Expr::Interpolated(parts);
1960 let result = eval_expr(&expr, &mut scope).unwrap();
1961 assert_eq!(result, Value::String("-hello-".into()));
1962 }
1963
1964 #[test]
1967 fn values_equal_scalars_still_work() {
1968 assert_eq!(
1969 values_equal(&Value::String("x".into()), &Value::String("x".into())),
1970 Ok(true)
1971 );
1972 assert_eq!(
1974 values_equal(&Value::String("42".into()), &Value::Int(42)),
1975 Ok(true)
1976 );
1977 }
1978
1979 #[test]
1980 fn values_equal_collection_vs_scalar_is_loud() {
1981 let list = Value::Json(serde_json::json!(["a", "b"]));
1982 let record = Value::Json(serde_json::json!({"k": 1}));
1983 assert!(
1984 matches!(values_equal(&list, &Value::String("banana".into())), Err(EvalError::Unsupported(_))),
1985 "list vs scalar must be a loud error, never silently false"
1986 );
1987 assert!(matches!(
1989 values_equal(&Value::String("x".into()), &record),
1990 Err(EvalError::Unsupported(_))
1991 ));
1992 }
1993
1994 #[test]
1995 fn values_equal_collection_vs_collection_is_structural() {
1996 let a = Value::Json(serde_json::json!({"a": 1, "b": 2}));
1998 let b = Value::Json(serde_json::json!({"b": 2, "a": 1}));
1999 assert_eq!(values_equal(&a, &b), Ok(true));
2000 }
2001
2002 #[test]
2005 fn values_equal_bytes_vs_bytes_still_works() {
2006 assert_eq!(
2008 values_equal(&Value::Bytes(vec![1, 2, 3]), &Value::Bytes(vec![1, 2, 3])),
2009 Ok(true)
2010 );
2011 assert_eq!(
2012 values_equal(&Value::Bytes(vec![1, 2, 3]), &Value::Bytes(vec![1, 2, 4])),
2013 Ok(false)
2014 );
2015 }
2016
2017 #[test]
2018 fn values_equal_bytes_vs_scalar_is_loud() {
2019 let bin = Value::Bytes(vec![0xff, 0x00]);
2023 assert!(matches!(
2024 values_equal(&bin, &Value::String("x".into())),
2025 Err(EvalError::Unsupported(_))
2026 ));
2027 assert!(matches!(
2028 values_equal(&Value::Int(1), &bin),
2029 Err(EvalError::Unsupported(_))
2030 ));
2031 }
2032
2033 #[test]
2034 fn eval_membership_bytes_needle_against_record_key_is_loud() {
2035 let record = Value::Json(serde_json::json!({"k": 1}));
2036 let bin = Value::Bytes(vec![0xff, 0x00]);
2037 assert!(matches!(
2038 eval_membership(&bin, &record),
2039 Err(EvalError::Unsupported(_))
2040 ));
2041 }
2042
2043 #[test]
2044 fn eval_membership_bytes_needle_against_list_is_not_a_match_not_an_abort() {
2045 let list = Value::Json(serde_json::json!(["a", "b"]));
2049 let bin = Value::Bytes(vec![0xff, 0x00]);
2050 assert_eq!(eval_membership(&bin, &list), Ok(false));
2051 }
2052
2053 #[test]
2054 fn value_length_of_bytes_is_byte_count() {
2055 assert_eq!(value_length(&Value::Bytes(vec![1, 2, 3])), 3);
2056 }
2057
2058 #[test]
2059 fn structured_export_error_flags_collections_passes_scalars() {
2060 let scalars = vec![
2062 ("A".to_string(), Value::String("x".into())),
2063 ("B".to_string(), Value::Int(1)),
2064 ];
2065 assert!(structured_export_error(&scalars).is_none());
2066 let with_record = vec![(
2068 "CFG".to_string(),
2069 Value::Json(serde_json::json!({"port": 8080})),
2070 )];
2071 let msg = structured_export_error(&with_record).expect("record must be refused");
2072 assert!(msg.contains("CFG") && msg.contains("tojson"), "got: {msg}");
2073 let with_list = vec![("XS".to_string(), Value::Json(serde_json::json!([1, 2])))];
2075 assert!(structured_export_error(&with_list).is_some());
2076 }
2077
2078 #[test]
2079 fn defaults_on_emptiness_matches_decision_a() {
2080 assert!(value_defaults_on_emptiness(&Value::Null));
2083 assert!(value_defaults_on_emptiness(&Value::Json(serde_json::Value::Null)));
2084 assert!(value_defaults_on_emptiness(&Value::String(String::new())));
2085 assert!(!value_defaults_on_emptiness(&Value::Bool(false)));
2086 assert!(!value_defaults_on_emptiness(&Value::Int(0)));
2087 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!([]))));
2088 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!({}))));
2089 assert!(!value_defaults_on_emptiness(&Value::String("x".into())));
2090 }
2091
2092 #[test]
2093 fn subscripted_length_and_default_resolve_the_path() {
2094 let mut scope = Scope::new();
2097 scope.set("u", Value::Json(serde_json::json!({"tags": ["a", "b"]})));
2098 let len = eval_expr(
2099 &Expr::VarLength(crate::parser::parse_varpath("${u[tags]}")),
2100 &mut scope,
2101 )
2102 .unwrap();
2103 assert_eq!(len, Value::Int(2));
2104
2105 scope.set("cfg", Value::Json(serde_json::json!({"port": 9000})));
2106 let val = eval_expr(
2108 &Expr::VarWithDefault {
2109 path: crate::parser::parse_varpath("${cfg[port]}"),
2110 default: vec![StringPart::Literal("8080".into())],
2111 },
2112 &mut scope,
2113 )
2114 .unwrap();
2115 assert_eq!(value_to_string(&val), "9000");
2116
2117 let missing = eval_expr(
2119 &Expr::VarWithDefault {
2120 path: crate::parser::parse_varpath("${cfg[nope]}"),
2121 default: vec![StringPart::Literal("8080".into())],
2122 },
2123 &mut scope,
2124 )
2125 .unwrap();
2126 assert_eq!(value_to_string(&missing), "8080");
2127
2128 let err = eval_expr(
2130 &Expr::VarWithDefault {
2131 path: crate::parser::parse_varpath("${cfg[0]}"),
2132 default: vec![StringPart::Literal("x".into())],
2133 },
2134 &mut scope,
2135 )
2136 .unwrap_err();
2137 assert!(matches!(err, EvalError::InvalidPath(_)), "got: {err}");
2138 }
2139}