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)]
111#[non_exhaustive]
112pub enum EvalError {
113 UndefinedVariable(String),
115 InvalidPath(String),
117 TypeError { expected: &'static str, got: String },
119 CommandFailed(String),
121 NoExecutor,
126 ArithmeticError(String),
128 RegexError(String),
130 Unsupported(String),
134}
135
136impl fmt::Display for EvalError {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 match self {
139 EvalError::UndefinedVariable(name) => write!(f, "undefined variable: {name}"),
140 EvalError::InvalidPath(path) => write!(f, "invalid path: {path}"),
141 EvalError::TypeError { expected, got } => {
142 write!(f, "type error: expected {expected}, got {got}")
143 }
144 EvalError::CommandFailed(msg) => write!(f, "command failed: {msg}"),
145 EvalError::NoExecutor => write!(
146 f,
147 "command substitution must be resolved by the async evaluator before sync evaluation"
148 ),
149 EvalError::ArithmeticError(msg) => write!(f, "arithmetic error: {msg}"),
150 EvalError::RegexError(msg) => write!(f, "regex error: {msg}"),
151 EvalError::Unsupported(msg) => write!(f, "{msg}"),
152 }
153 }
154}
155
156impl std::error::Error for EvalError {}
157
158pub type EvalResult<T> = Result<T, EvalError>;
160
161pub struct Evaluator<'a> {
168 scope: &'a mut Scope,
169}
170
171impl<'a> Evaluator<'a> {
172 pub fn new(scope: &'a mut Scope) -> Self {
174 Self { scope }
175 }
176
177 pub fn eval(&mut self, expr: &Expr) -> EvalResult<Value> {
179 match expr {
180 Expr::Not(inner) => Ok(Value::Bool(!is_truthy(&self.eval(inner)?))),
183 Expr::Literal(value) => self.eval_literal(value),
184 Expr::VarRef(path) => self.eval_var_ref(path),
185 Expr::Interpolated(parts) => self.eval_interpolated(parts),
186 Expr::HereDocBody { parts, strip_tabs } => {
187 let mut asm = HeredocAssembler::new(*strip_tabs);
190 for sp in parts {
191 match &sp.part {
192 StringPart::Literal(s) => asm.push_literal(s),
193 other => {
194 let value = self.eval_interpolated(std::slice::from_ref(other))?;
201 asm.push_interpolated(&value_to_text_sink(&value)?);
202 }
203 }
204 }
205 Ok(Value::String(asm.into_string()))
206 }
207 Expr::BinaryOp { left, op, right } => self.eval_binary_op(left, *op, right),
208 Expr::CommandSubst(_) => Err(EvalError::NoExecutor),
212 Expr::Test(test_expr) => self.eval_test(test_expr),
213 Expr::Positional(n) => self.eval_positional(*n),
214 Expr::AllArgs => self.eval_all_args(),
215 Expr::ArgCount => self.eval_arg_count(),
216 Expr::VarLength(path) => self.eval_var_length(path),
217 Expr::VarWithDefault { path, default } => self.eval_var_with_default(path, default),
218 Expr::Arithmetic(expr_str) => self.eval_arithmetic(expr_str),
219 Expr::Command(cmd) => self.eval_command(cmd),
220 Expr::LastExitCode => self.eval_last_exit_code(),
221 Expr::CurrentPid => self.eval_current_pid(),
222 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
223 Expr::ListLiteral(elems) => self.eval_list_literal(elems),
224 Expr::RecordLiteral(entries) => self.eval_record_literal(entries),
225 }
226 }
227
228 fn eval_list_literal(&mut self, elems: &[ListElem]) -> EvalResult<Value> {
232 let mut out = Vec::with_capacity(elems.len());
233 for elem in elems {
234 match elem {
235 ListElem::Item(e) => {
236 let value = self.eval(e)?;
237 out.push(kaish_types::value_to_json(&value));
238 }
239 ListElem::Spread(e) => {
240 let value = self.eval(e)?;
241 match value {
242 Value::Json(serde_json::Value::Array(items)) => out.extend(items),
243 other => return Err(EvalError::Unsupported(spread_non_list_message(&other))),
244 }
245 }
246 }
247 }
248 Ok(Value::Json(serde_json::Value::Array(out)))
249 }
250
251 fn eval_record_literal(&mut self, entries: &[RecordEntry]) -> EvalResult<Value> {
256 let mut map = serde_json::Map::new();
257 for entry in entries {
258 let key = match &entry.key {
259 RecordKey::Bare(s) | RecordKey::Quoted(s) => s.clone(),
260 RecordKey::Interpolated(parts) => {
267 value_to_text_sink(&self.eval_interpolated(parts)?)?
268 }
269 };
270 let value = self.eval(&entry.value)?;
271 map.insert(key, kaish_types::value_to_json(&value));
272 }
273 Ok(Value::Json(serde_json::Value::Object(map)))
274 }
275
276 fn eval_last_exit_code(&self) -> EvalResult<Value> {
278 Ok(Value::Int(self.scope.last_result().code))
279 }
280
281 fn eval_current_pid(&self) -> EvalResult<Value> {
283 Ok(Value::Int(self.scope.pid() as i64))
284 }
285
286 fn eval_command(&mut self, cmd: &crate::ast::Command) -> EvalResult<Value> {
288 match cmd.name.as_str() {
291 "true" => Ok(Value::Bool(true)),
292 "false" => Ok(Value::Bool(false)),
293 _ => Err(EvalError::NoExecutor),
297 }
298 }
299
300 fn eval_arithmetic(&mut self, expr_str: &str) -> EvalResult<Value> {
302 arithmetic::eval_arithmetic(expr_str, self.scope)
303 .map(Value::Int)
304 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
305 }
306
307 fn eval_test(&mut self, test_expr: &TestExpr) -> EvalResult<Value> {
309 let result = match test_expr {
310 TestExpr::FileTest { .. } => {
311 return Err(EvalError::Unsupported(
318 "file tests must be resolved by the async evaluator".to_string(),
319 ));
320 }
321 TestExpr::StringTest { op, value } => match op {
322 StringTestOp::IsEmpty | StringTestOp::IsNonEmpty => {
323 let val = self.eval(value)?;
324 let symbol = match op {
328 StringTestOp::IsEmpty => "-z",
329 StringTestOp::IsNonEmpty => "-n",
330 StringTestOp::IsList | StringTestOp::IsRecord => unreachable!(),
331 };
332 if let Some(msg) = scalar_test_operand_error(symbol, &val) {
333 return Err(EvalError::Unsupported(msg));
334 }
335 let s = value_to_string(&val);
336 match op {
337 StringTestOp::IsEmpty => s.is_empty(),
338 StringTestOp::IsNonEmpty => !s.is_empty(),
339 StringTestOp::IsList | StringTestOp::IsRecord => unreachable!(),
340 }
341 }
342 StringTestOp::IsList | StringTestOp::IsRecord => {
349 let val = self.eval(value)?;
350 op.matches_shape(&val)
351 }
352 },
353 TestExpr::Comparison { left, op, right } => {
354 let left_val = self.eval(left)?;
355 let right_val = self.eval(right)?;
356
357 match op {
358 TestCmpOp::Eq => values_equal(&left_val, &right_val)?,
359 TestCmpOp::NotEq => !(values_equal(&left_val, &right_val)?),
360 TestCmpOp::Match => {
361 guard_scalar_test_operands(op, &left_val, &right_val)?;
363 match regex_match(&left_val, &right_val, false)? {
365 Value::Bool(b) => b,
366 _ => false,
367 }
368 }
369 TestCmpOp::NotMatch => {
370 guard_scalar_test_operands(op, &left_val, &right_val)?;
371 match regex_match(&left_val, &right_val, true)? {
373 Value::Bool(b) => b,
374 _ => true,
375 }
376 }
377 TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
378 guard_scalar_test_operands(op, &left_val, &right_val)?;
380 let ord = compare_values(&left_val, &right_val)?;
382 match op {
383 TestCmpOp::Gt => ord.is_gt(),
384 TestCmpOp::Lt => ord.is_lt(),
385 TestCmpOp::GtEq => ord.is_ge(),
386 TestCmpOp::LtEq => ord.is_le(),
387 _ => unreachable!(),
388 }
389 }
390 TestCmpOp::NumEq
391 | TestCmpOp::NumNotEq
392 | TestCmpOp::NumGt
393 | TestCmpOp::NumLt
394 | TestCmpOp::NumGtEq
395 | TestCmpOp::NumLtEq => {
396 guard_scalar_test_operands(op, &left_val, &right_val)?;
398 let ord = numeric_compare(&left_val, &right_val)?;
401 match op {
402 TestCmpOp::NumEq => ord.is_eq(),
403 TestCmpOp::NumNotEq => !ord.is_eq(),
404 TestCmpOp::NumGt => ord.is_gt(),
405 TestCmpOp::NumLt => ord.is_lt(),
406 TestCmpOp::NumGtEq => ord.is_ge(),
407 TestCmpOp::NumLtEq => ord.is_le(),
408 _ => unreachable!(),
409 }
410 }
411 }
412 }
413 TestExpr::And { left, right } => {
414 let left_result = self.eval_test(left)?;
416 if !value_to_bool(&left_result) {
417 false } else {
419 value_to_bool(&self.eval_test(right)?)
420 }
421 }
422 TestExpr::Or { left, right } => {
423 let left_result = self.eval_test(left)?;
425 if value_to_bool(&left_result) {
426 true } else {
428 value_to_bool(&self.eval_test(right)?)
429 }
430 }
431 TestExpr::Not { expr } => {
432 let result = self.eval_test(expr)?;
433 !value_to_bool(&result)
434 }
435 TestExpr::In { left, right } => {
436 let left_val = self.eval(left)?;
437 let right_val = self.eval(right)?;
438 eval_membership(&left_val, &right_val)?
439 }
440 TestExpr::NotIn { left, right } => {
441 let left_val = self.eval(left)?;
442 let right_val = self.eval(right)?;
443 !eval_membership(&left_val, &right_val)?
444 }
445 };
446 Ok(Value::Bool(result))
447 }
448
449 fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
451 Ok(value.clone())
452 }
453
454 fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
456 match self.scope.resolve_path(path) {
457 Ok(v) => Ok(v),
458 Err(super::scope::PathError::UndefinedRoot(_)) => {
460 Err(EvalError::InvalidPath(format_path(path)))
461 }
462 Err(super::scope::PathError::Absence(msg))
465 | Err(super::scope::PathError::Shape(msg)) => Err(EvalError::InvalidPath(msg)),
466 }
467 }
468
469 fn eval_positional(&self, n: usize) -> EvalResult<Value> {
471 match self.scope.get_positional(n) {
472 Some(s) => Ok(Value::String(s.to_string())),
473 None => Ok(Value::String(String::new())), }
475 }
476
477 fn eval_all_args(&self) -> EvalResult<Value> {
481 let args = self.scope.all_args();
482 Ok(Value::String(args.join(" ")))
483 }
484
485 fn eval_arg_count(&self) -> EvalResult<Value> {
487 Ok(Value::Int(self.scope.arg_count() as i64))
488 }
489
490 fn eval_var_length(&self, path: &VarPath) -> EvalResult<Value> {
492 resolve_length(self.scope, path)
493 .map(Value::Int)
494 .map_err(EvalError::InvalidPath)
495 }
496
497 fn eval_var_with_default(&mut self, path: &VarPath, default: &[StringPart]) -> EvalResult<Value> {
501 match resolve_default(self.scope, path).map_err(EvalError::InvalidPath)? {
502 Some(value) => Ok(value),
503 None => self.eval_interpolated(default),
504 }
505 }
506
507 fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
509 let mut result = String::new();
510 for part in parts {
511 match part {
512 StringPart::Literal(s) => result.push_str(s),
513 StringPart::Var(path) => {
514 match self.scope.resolve_path(path) {
515 Ok(value) => result.push_str(&value_to_text_sink(&value)?),
517 Err(super::scope::PathError::UndefinedRoot(_)) => {}
519 Err(super::scope::PathError::Absence(msg))
522 | Err(super::scope::PathError::Shape(msg)) => {
523 return Err(EvalError::InvalidPath(msg))
524 }
525 }
526 }
527 StringPart::VarWithDefault { path, default } => {
528 let value = self.eval_var_with_default(path, default)?;
529 result.push_str(&value_to_text_sink(&value)?);
530 }
531 StringPart::VarLength(path) => {
532 let value = self.eval_var_length(path)?;
533 result.push_str(&value_to_text_sink(&value)?);
534 }
535 StringPart::Positional(n) => {
536 let value = self.eval_positional(*n)?;
537 result.push_str(&value_to_text_sink(&value)?);
538 }
539 StringPart::AllArgs => {
540 let value = self.eval_all_args()?;
541 result.push_str(&value_to_text_sink(&value)?);
542 }
543 StringPart::ArgCount => {
544 let value = self.eval_arg_count()?;
545 result.push_str(&value_to_text_sink(&value)?);
546 }
547 StringPart::Arithmetic(expr) => {
548 let value = self.eval_arithmetic_string(expr)?;
550 result.push_str(&value_to_text_sink(&value)?);
551 }
552 StringPart::CommandSubst(_) => {
553 return Err(EvalError::NoExecutor);
559 }
560 StringPart::LastExitCode => {
561 result.push_str(&self.scope.last_result().code.to_string());
562 }
563 StringPart::CurrentPid => {
564 result.push_str(&self.scope.pid().to_string());
565 }
566 }
567 }
568 Ok(Value::String(result))
569 }
570
571 fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
573 arithmetic::eval_arithmetic(expr, self.scope)
575 .map(Value::Int)
576 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
577 }
578
579 fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
583 match op {
584 BinaryOp::And => {
585 let left_val = self.eval(left)?;
586 if !is_truthy(&left_val) {
587 return Ok(left_val);
588 }
589 self.eval(right)
590 }
591 BinaryOp::Or => {
592 let left_val = self.eval(left)?;
593 if is_truthy(&left_val) {
594 return Ok(left_val);
595 }
596 self.eval(right)
597 }
598 }
599 }
600
601}
602
603pub fn value_to_exit_code(value: &Value) -> anyhow::Result<i64> {
610 match value {
611 Value::Int(n) => Ok(*n),
612 Value::Bool(b) => Ok(if *b { 0 } else { 1 }),
613 Value::Float(f) => Ok(*f as i64),
614 Value::String(s) => {
615 let trimmed = s.trim();
616 trimmed.parse::<i64>().map_err(|_| {
617 anyhow::anyhow!("numeric argument required: {:?}", s)
618 })
619 }
620 Value::Null | Value::Json(_) | Value::Bytes(_) => {
621 anyhow::bail!("numeric argument required (got {:?})", value)
622 }
623 }
624}
625
626pub fn value_length(value: &Value) -> i64 {
639 match value {
640 Value::Json(serde_json::Value::Array(a)) => a.len() as i64,
641 Value::Json(serde_json::Value::Object(o)) => o.len() as i64,
642 Value::Bytes(b) => b.len() as i64,
646 Value::String(s) => s.chars().count() as i64,
649 other => value_to_string(other).chars().count() as i64,
650 }
651}
652
653pub fn value_defaults_on_emptiness(value: &Value) -> bool {
660 match value {
661 Value::Null | Value::Json(serde_json::Value::Null) => true,
662 Value::String(s) => s.is_empty(),
663 _ => false,
664 }
665}
666
667pub fn resolve_length(scope: &Scope, path: &VarPath) -> Result<i64, String> {
674 match scope.resolve_path(path) {
675 Ok(value) => Ok(value_length(&value)),
676 Err(super::scope::PathError::UndefinedRoot(_)) if path.segments.len() <= 1 => Ok(0),
677 Err(super::scope::PathError::UndefinedRoot(_)) => {
678 Err(format!("{}: undefined variable", format_path(path)))
679 }
680 Err(super::scope::PathError::Absence(msg)) | Err(super::scope::PathError::Shape(msg)) => {
681 Err(msg)
682 }
683 }
684}
685
686pub fn resolve_default(scope: &Scope, path: &VarPath) -> Result<Option<Value>, String> {
692 match scope.resolve_path(path) {
693 Ok(value) if value_defaults_on_emptiness(&value) => Ok(None),
694 Ok(value) => Ok(Some(value)),
695 Err(super::scope::PathError::UndefinedRoot(_))
696 | Err(super::scope::PathError::Absence(_)) => Ok(None),
697 Err(super::scope::PathError::Shape(msg)) => Err(msg),
698 }
699}
700
701pub fn structured_export_error(vars: &[(String, Value)]) -> Option<String> {
707 for (name, value) in vars {
708 if let Value::Json(j) = value {
709 if matches!(j, serde_json::Value::Array(_) | serde_json::Value::Object(_)) {
710 let kind = if j.is_array() { "list" } else { "record" };
711 return Some(format!(
712 "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})`"
713 ));
714 }
715 }
716 }
717 None
718}
719
720pub fn is_collection(value: &Value) -> bool {
725 matches!(
726 value,
727 Value::Json(serde_json::Value::Array(_)) | Value::Json(serde_json::Value::Object(_))
728 )
729}
730
731fn collection_kind(value: &Value) -> &'static str {
734 match value {
735 Value::Json(serde_json::Value::Array(_)) => "list",
736 Value::Json(serde_json::Value::Object(_)) => "record",
737 _ => "collection",
738 }
739}
740
741pub fn structured_boundary_error(sink: &str, value: &Value) -> Option<String> {
751 if is_collection(value) {
752 let kind = collection_kind(value);
753 Some(format!(
754 "cannot use a {kind} as {sink} — serialize it explicitly first, e.g. `cmd $(tojson $x)`"
755 ))
756 } else {
757 None
758 }
759}
760
761pub fn scalar_test_operand_error(op_symbol: &str, value: &Value) -> Option<String> {
770 if is_collection(value) {
771 let kind = collection_kind(value);
772 Some(format!(
773 "`{op_symbol}` needs a scalar; got a {kind} — use `${{#x}}` for length, \
774 `-list`/`-record` to test shape, or `in` for membership"
775 ))
776 } else {
777 None
778 }
779}
780
781pub fn value_to_string(value: &Value) -> String {
782 match value {
783 Value::Null => "null".to_string(),
784 Value::Bool(b) => b.to_string(),
785 Value::Int(i) => i.to_string(),
786 Value::Float(f) => f.to_string(),
787 Value::String(s) => s.clone(),
788 Value::Json(json) => json.to_string(),
789 Value::Bytes(b) => format!("[binary: {} bytes]", b.len()),
795 }
796}
797
798pub fn value_to_text_sink(value: &Value) -> EvalResult<String> {
817 value_to_text_sink_named(value, "text")
818}
819
820pub fn value_to_text_sink_named(value: &Value, sink: &str) -> EvalResult<String> {
829 match value {
830 Value::Bytes(b) => match std::str::from_utf8(b) {
831 Ok(s) => Ok(s.to_string()),
832 Err(_) => Err(EvalError::Unsupported(format!(
833 "binary data ({} bytes) cannot be used as {sink} — decode it \
834 (base64/xxd) or redirect to a file",
835 b.len()
836 ))),
837 },
838 other => Ok(value_to_string(other)),
839 }
840}
841
842pub fn values_to_text_sink_named(values: &[Value], sink: &str) -> EvalResult<Vec<String>> {
846 values.iter().map(|v| value_to_text_sink_named(v, sink)).collect()
847}
848
849pub fn value_to_bool(value: &Value) -> bool {
859 match value {
860 Value::Null => false,
861 Value::Bool(b) => *b,
862 Value::Int(i) => *i != 0,
863 Value::Float(f) => *f != 0.0,
864 Value::String(s) => !s.is_empty(),
865 Value::Json(json) => match json {
866 serde_json::Value::Null => false,
867 serde_json::Value::Array(arr) => !arr.is_empty(),
868 serde_json::Value::Object(obj) => !obj.is_empty(),
869 serde_json::Value::Bool(b) => *b,
870 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
871 serde_json::Value::String(s) => !s.is_empty(),
872 },
873 Value::Bytes(b) => !b.is_empty(), }
875}
876
877pub fn expand_tilde(s: &str, home: Option<&str>) -> String {
891 if s == "~" {
892 home.map(|h| h.to_string()).unwrap_or_else(|| "~".to_string())
893 } else if s.starts_with("~/") {
894 match home {
895 Some(home) => format!("{}{}", home, &s[1..]),
896 None => s.to_string(),
897 }
898 } else if s.starts_with('~') {
899 expand_tilde_user(s)
901 } else {
902 s.to_string()
903 }
904}
905
906#[cfg(all(unix, feature = "host"))]
911fn expand_tilde_user(s: &str) -> String {
912 let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
914 (&s[1..slash_pos + 1], &s[slash_pos + 1..])
915 } else {
916 (&s[1..], "")
917 };
918
919 if username.is_empty() {
920 return s.to_string();
921 }
922
923 let passwd = match std::fs::read_to_string("/etc/passwd") {
926 Ok(content) => content,
927 Err(_) => return s.to_string(),
928 };
929
930 for line in passwd.lines() {
931 let fields: Vec<&str> = line.split(':').collect();
932 if fields.len() >= 6 && fields[0] == username {
933 let home_dir = fields[5];
934 return if rest.is_empty() {
935 home_dir.to_string()
936 } else {
937 format!("{}{}", home_dir, rest)
938 };
939 }
940 }
941
942 s.to_string()
944}
945
946#[cfg(not(all(unix, feature = "host")))]
947fn expand_tilde_user(s: &str) -> String {
948 s.to_string()
951}
952
953pub fn value_to_string_with_tilde(value: &Value, home: Option<&str>) -> String {
958 match value {
959 Value::String(s) if s.starts_with('~') => expand_tilde(s, home),
960 _ => value_to_string(value),
961 }
962}
963
964pub(crate) fn format_path(path: &VarPath) -> String {
969 use crate::ast::VarSegment;
970 let mut result = String::from("${");
971 for (i, seg) in path.segments.iter().enumerate() {
972 match seg {
973 VarSegment::Field(name) => {
974 if i > 0 {
975 result.push('.');
976 }
977 result.push_str(name);
978 }
979 VarSegment::Index(idx) => result.push_str(&format!("[{idx}]")),
980 VarSegment::Key(k) => result.push_str(&format!("[{k}]")),
981 VarSegment::Dynamic(v) => result.push_str(&format!("[${v}]")),
982 VarSegment::Slice(a, b) => {
983 let s = a.map(|n| n.to_string()).unwrap_or_default();
984 let e = b.map(|n| n.to_string()).unwrap_or_default();
985 result.push_str(&format!("[{s}:{e}]"));
986 }
987 }
988 }
989 result.push('}');
990 result
991}
992
993fn is_truthy(value: &Value) -> bool {
1003 value_to_bool(value)
1005}
1006
1007pub fn values_equal(left: &Value, right: &Value) -> EvalResult<bool> {
1018 match (left, right) {
1019 (Value::Null, Value::Null) => Ok(true),
1020 (Value::Bool(a), Value::Bool(b)) => Ok(a == b),
1021 (Value::Int(a), Value::Int(b)) => Ok(a == b),
1022 (Value::Float(a), Value::Float(b)) => Ok((a - b).abs() < f64::EPSILON),
1023 (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
1024 Ok((*a as f64 - b).abs() < f64::EPSILON)
1025 }
1026 (Value::String(a), Value::String(b)) => Ok(a == b),
1027 (Value::Json(a), Value::Json(b)) => Ok(a == b),
1028 (Value::Bytes(a), Value::Bytes(b)) => Ok(a == b),
1029 (Value::Json(j), other) | (other, Value::Json(j))
1035 if matches!(j, serde_json::Value::Array(_) | serde_json::Value::Object(_)) =>
1036 {
1037 let kind = if j.is_array() { "list" } else { "record" };
1038 Err(EvalError::Unsupported(format!(
1039 "cannot compare a {kind} to a {other_kind} with ==/!= — test membership with `[[ x in $coll ]]`, or compare structures with `jq`",
1040 other_kind = type_name(other),
1041 )))
1042 }
1043 (Value::Bytes(b), other) | (other, Value::Bytes(b)) => Err(EvalError::Unsupported(format!(
1049 "binary data ({} bytes) cannot be used as an ==/!= operand against a {} — decode it \
1050 first (base64/xxd), or compare two binary values directly",
1051 b.len(),
1052 type_name(other),
1053 ))),
1054 _ => Ok(value_to_string(left) == value_to_string(right)),
1057 }
1058}
1059
1060fn element_matches(needle: &Value, element: &Value) -> bool {
1069 match (needle, element) {
1070 (Value::Json(a), Value::Json(b)) => a == b,
1071 (Value::Json(_), _) | (_, Value::Json(_)) => false,
1072 _ => values_equal(needle, element).unwrap_or(false),
1079 }
1080}
1081
1082fn eval_membership(needle: &Value, haystack: &Value) -> EvalResult<bool> {
1092 match haystack {
1093 Value::Json(serde_json::Value::Array(items)) => {
1094 for item in items {
1095 let element = json_to_value_no_envelope(item.clone());
1096 if element_matches(needle, &element) {
1097 return Ok(true);
1098 }
1099 }
1100 Ok(false)
1101 }
1102 Value::Json(serde_json::Value::Object(map)) => {
1103 if let Value::Bytes(b) = needle {
1108 return Err(EvalError::Unsupported(format!(
1109 "binary data ({} bytes) cannot be used as a record key for `in` — \
1110 decode it first (base64/xxd)",
1111 b.len()
1112 )));
1113 }
1114 Ok(map.contains_key(&value_to_string(needle)))
1115 }
1116 other => Err(EvalError::Unsupported(format!(
1117 "`in` requires a list or record on the right-hand side, got {} — substring tests use `=~`, glob (`[[ $s == *sub* ]]`), or `case`",
1118 type_name(other),
1119 ))),
1120 }
1121}
1122
1123fn cmp_op_symbol(op: &TestCmpOp) -> &'static str {
1126 match op {
1127 TestCmpOp::Eq => "==",
1128 TestCmpOp::NotEq => "!=",
1129 TestCmpOp::Match => "=~",
1130 TestCmpOp::NotMatch => "!~",
1131 TestCmpOp::Gt => ">",
1132 TestCmpOp::Lt => "<",
1133 TestCmpOp::GtEq => ">=",
1134 TestCmpOp::LtEq => "<=",
1135 TestCmpOp::NumEq => "-eq",
1136 TestCmpOp::NumNotEq => "-ne",
1137 TestCmpOp::NumGt => "-gt",
1138 TestCmpOp::NumLt => "-lt",
1139 TestCmpOp::NumGtEq => "-ge",
1140 TestCmpOp::NumLtEq => "-le",
1141 }
1142}
1143
1144fn guard_scalar_test_operands(op: &TestCmpOp, left: &Value, right: &Value) -> EvalResult<()> {
1148 let symbol = cmp_op_symbol(op);
1149 if let Some(msg) = scalar_test_operand_error(symbol, left) {
1150 return Err(EvalError::Unsupported(msg));
1151 }
1152 if let Some(msg) = scalar_test_operand_error(symbol, right) {
1153 return Err(EvalError::Unsupported(msg));
1154 }
1155 Ok(())
1156}
1157
1158fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1160 match (left, right) {
1161 (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
1162 (Value::Float(a), Value::Float(b)) => {
1163 a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1164 }
1165 (Value::Int(a), Value::Float(b)) => {
1166 (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1167 }
1168 (Value::Float(a), Value::Int(b)) => {
1169 a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1170 }
1171 (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
1172 _ => Err(EvalError::TypeError {
1173 expected: "comparable types (numbers or strings)",
1174 got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
1175 }),
1176 }
1177}
1178
1179enum Num {
1184 Int(i64),
1185 Float(f64),
1186}
1187
1188fn value_to_num(value: &Value) -> EvalResult<Num> {
1189 match value {
1190 Value::Int(n) => Ok(Num::Int(*n)),
1191 Value::Float(f) => Ok(Num::Float(*f)),
1192 Value::String(s) => {
1193 let t = s.trim();
1194 if let Ok(n) = t.parse::<i64>() {
1195 Ok(Num::Int(n))
1196 } else if let Ok(f) = t.parse::<f64>() {
1197 Ok(Num::Float(f))
1198 } else {
1199 Err(EvalError::TypeError {
1200 expected: "numeric operand",
1201 got: format!("non-numeric string {:?}", s),
1202 })
1203 }
1204 }
1205 _ => Err(EvalError::TypeError {
1206 expected: "numeric operand",
1207 got: type_name(value).to_string(),
1208 }),
1209 }
1210}
1211
1212pub fn numeric_compare(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1217 let l = value_to_num(left)?;
1218 let r = value_to_num(right)?;
1219 match (l, r) {
1220 (Num::Int(a), Num::Int(b)) => Ok(a.cmp(&b)),
1221 (Num::Float(a), Num::Float(b)) => a
1222 .partial_cmp(&b)
1223 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1224 (Num::Int(a), Num::Float(b)) => (a as f64)
1225 .partial_cmp(&b)
1226 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1227 (Num::Float(a), Num::Int(b)) => a
1228 .partial_cmp(&(b as f64))
1229 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1230 }
1231}
1232
1233fn type_name(value: &Value) -> &'static str {
1235 match value {
1236 Value::Null => "null",
1237 Value::Bool(_) => "bool",
1238 Value::Int(_) => "int",
1239 Value::Float(_) => "float",
1240 Value::String(_) => "string",
1241 Value::Json(_) => "json",
1242 Value::Bytes(_) => "bytes",
1243 }
1244}
1245
1246fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
1251 let text = match left {
1252 Value::String(s) => s.as_str(),
1253 _ => {
1254 return Err(EvalError::TypeError {
1255 expected: "string",
1256 got: type_name(left).to_string(),
1257 })
1258 }
1259 };
1260
1261 let pattern = match right {
1262 Value::String(s) => s.as_str(),
1263 _ => {
1264 return Err(EvalError::TypeError {
1265 expected: "string (regex pattern)",
1266 got: type_name(right).to_string(),
1267 })
1268 }
1269 };
1270
1271 let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
1272 let matches = re.is_match(text);
1273
1274 Ok(Value::Bool(if negate { !matches } else { matches }))
1275}
1276
1277pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
1284 let mut evaluator = Evaluator::new(scope);
1285 evaluator.eval(expr)
1286}
1287
1288#[cfg(test)]
1289#[allow(clippy::approx_constant)]
1290mod tests {
1291 use super::*;
1292 use crate::ast::{Stmt, VarSegment};
1293 use super::super::result::ExecResult;
1294
1295 fn var_expr(name: &str) -> Expr {
1297 Expr::VarRef(VarPath::simple(name))
1298 }
1299
1300 #[test]
1301 fn eval_literal_int() {
1302 let mut scope = Scope::new();
1303 let expr = Expr::Literal(Value::Int(42));
1304 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1305 }
1306
1307 #[test]
1308 fn eval_literal_string() {
1309 let mut scope = Scope::new();
1310 let expr = Expr::Literal(Value::String("hello".into()));
1311 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
1312 }
1313
1314 #[test]
1315 fn eval_literal_bool() {
1316 let mut scope = Scope::new();
1317 assert_eq!(
1318 eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
1319 Ok(Value::Bool(true))
1320 );
1321 }
1322
1323 #[test]
1324 fn eval_literal_null() {
1325 let mut scope = Scope::new();
1326 assert_eq!(
1327 eval_expr(&Expr::Literal(Value::Null), &mut scope),
1328 Ok(Value::Null)
1329 );
1330 }
1331
1332 #[test]
1333 fn eval_literal_float() {
1334 let mut scope = Scope::new();
1335 let expr = Expr::Literal(Value::Float(3.14));
1336 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
1337 }
1338
1339 #[test]
1340 fn eval_variable_ref() {
1341 let mut scope = Scope::new();
1342 scope.set("X", Value::Int(100));
1343 assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
1344 }
1345
1346 #[test]
1347 fn eval_undefined_variable() {
1348 let mut scope = Scope::new();
1349 let result = eval_expr(&var_expr("MISSING"), &mut scope);
1350 assert!(matches!(result, Err(EvalError::InvalidPath(_))));
1351 }
1352
1353 #[test]
1354 fn eval_interpolated_string() {
1355 let mut scope = Scope::new();
1356 scope.set("NAME", Value::String("World".into()));
1357
1358 let expr = Expr::Interpolated(vec![
1359 StringPart::Literal("Hello, ".into()),
1360 StringPart::Var(VarPath::simple("NAME")),
1361 StringPart::Literal("!".into()),
1362 ]);
1363 assert_eq!(
1364 eval_expr(&expr, &mut scope),
1365 Ok(Value::String("Hello, World!".into()))
1366 );
1367 }
1368
1369 #[test]
1381 fn eval_heredoc_body_binary_var_is_loud() {
1382 let mut scope = Scope::new();
1383 scope.set("B", Value::Bytes(vec![0xff, 0x00, 0xfe]));
1384
1385 let expr = Expr::HereDocBody {
1386 parts: vec![
1387 crate::ast::SpannedPart {
1388 part: StringPart::Literal("before ".into()),
1389 offset: 0,
1390 len: 0,
1391 },
1392 crate::ast::SpannedPart {
1393 part: StringPart::Var(VarPath::simple("B")),
1394 offset: 0,
1395 len: 0,
1396 },
1397 ],
1398 strip_tabs: false,
1399 };
1400 let err = eval_expr(&expr, &mut scope).expect_err("binary in a heredoc body must be loud");
1401 assert!(
1402 matches!(err, EvalError::Unsupported(ref msg) if msg.contains("cannot be used as")),
1403 "got {err:?}"
1404 );
1405 }
1406
1407 #[test]
1408 fn eval_heredoc_body_text_var_is_unaffected() {
1409 let mut scope = Scope::new();
1410 scope.set("NAME", Value::String("World".into()));
1411
1412 let expr = Expr::HereDocBody {
1413 parts: vec![
1414 crate::ast::SpannedPart {
1415 part: StringPart::Literal("Hello, ".into()),
1416 offset: 0,
1417 len: 0,
1418 },
1419 crate::ast::SpannedPart {
1420 part: StringPart::Var(VarPath::simple("NAME")),
1421 offset: 0,
1422 len: 0,
1423 },
1424 ],
1425 strip_tabs: false,
1426 };
1427 assert_eq!(
1428 eval_expr(&expr, &mut scope),
1429 Ok(Value::String("Hello, World".into()))
1430 );
1431 }
1432
1433 #[test]
1434 fn eval_record_literal_interpolated_key_binary_var_is_loud() {
1435 let mut scope = Scope::new();
1436 scope.set("B", Value::Bytes(vec![0xff, 0x00, 0xfe]));
1437
1438 let expr = Expr::RecordLiteral(vec![RecordEntry {
1439 key: RecordKey::Interpolated(vec![StringPart::Var(VarPath::simple("B"))]),
1440 value: Expr::Literal(Value::Int(1)),
1441 }]);
1442 let err = eval_expr(&expr, &mut scope)
1443 .expect_err("a binary record key must be loud, not a `[binary: N bytes]` key");
1444 assert!(
1445 matches!(err, EvalError::Unsupported(ref msg) if msg.contains("cannot be used as")),
1446 "got {err:?}"
1447 );
1448 }
1449
1450 #[test]
1451 fn eval_record_literal_interpolated_key_text_var_is_unaffected() {
1452 let mut scope = Scope::new();
1453 scope.set("K", Value::String("port".into()));
1454
1455 let expr = Expr::RecordLiteral(vec![RecordEntry {
1456 key: RecordKey::Interpolated(vec![StringPart::Var(VarPath::simple("K"))]),
1457 value: Expr::Literal(Value::Int(8080)),
1458 }]);
1459 assert_eq!(
1460 eval_expr(&expr, &mut scope),
1461 Ok(Value::Json(serde_json::json!({"port": 8080})))
1462 );
1463 }
1464
1465 #[test]
1466 fn eval_interpolated_with_number() {
1467 let mut scope = Scope::new();
1468 scope.set("COUNT", Value::Int(42));
1469
1470 let expr = Expr::Interpolated(vec![
1471 StringPart::Literal("Count: ".into()),
1472 StringPart::Var(VarPath::simple("COUNT")),
1473 ]);
1474 assert_eq!(
1475 eval_expr(&expr, &mut scope),
1476 Ok(Value::String("Count: 42".into()))
1477 );
1478 }
1479
1480 #[test]
1481 fn eval_and_short_circuit_true() {
1482 let mut scope = Scope::new();
1483 let expr = Expr::BinaryOp {
1484 left: Box::new(Expr::Literal(Value::Bool(true))),
1485 op: BinaryOp::And,
1486 right: Box::new(Expr::Literal(Value::Int(42))),
1487 };
1488 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1490 }
1491
1492 #[test]
1493 fn eval_and_short_circuit_false() {
1494 let mut scope = Scope::new();
1495 let expr = Expr::BinaryOp {
1496 left: Box::new(Expr::Literal(Value::Bool(false))),
1497 op: BinaryOp::And,
1498 right: Box::new(Expr::Literal(Value::Int(42))),
1499 };
1500 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
1502 }
1503
1504 #[test]
1505 fn eval_or_short_circuit_true() {
1506 let mut scope = Scope::new();
1507 let expr = Expr::BinaryOp {
1508 left: Box::new(Expr::Literal(Value::Bool(true))),
1509 op: BinaryOp::Or,
1510 right: Box::new(Expr::Literal(Value::Int(42))),
1511 };
1512 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1514 }
1515
1516 #[test]
1517 fn eval_or_short_circuit_false() {
1518 let mut scope = Scope::new();
1519 let expr = Expr::BinaryOp {
1520 left: Box::new(Expr::Literal(Value::Bool(false))),
1521 op: BinaryOp::Or,
1522 right: Box::new(Expr::Literal(Value::Int(42))),
1523 };
1524 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1526 }
1527
1528 #[test]
1529 fn is_truthy_values() {
1530 assert!(!is_truthy(&Value::Null));
1531 assert!(!is_truthy(&Value::Bool(false)));
1532 assert!(is_truthy(&Value::Bool(true)));
1533 assert!(!is_truthy(&Value::Int(0)));
1534 assert!(is_truthy(&Value::Int(1)));
1535 assert!(is_truthy(&Value::Int(-1)));
1536 assert!(!is_truthy(&Value::Float(0.0)));
1537 assert!(is_truthy(&Value::Float(0.1)));
1538 assert!(!is_truthy(&Value::String("".into())));
1539 assert!(is_truthy(&Value::String("x".into())));
1540 }
1541
1542 #[test]
1543 fn sync_command_subst_is_loud_not_silent() {
1544 use crate::ast::Command;
1548
1549 let mut scope = Scope::new();
1550 let expr = Expr::CommandSubst(vec![Stmt::Command(Command {
1551 name: "echo".into(),
1552 args: vec![],
1553 redirects: vec![],
1554 })]);
1555
1556 assert!(matches!(
1557 eval_expr(&expr, &mut scope),
1558 Err(EvalError::NoExecutor)
1559 ));
1560 }
1561
1562 #[test]
1563 fn eval_last_result_bare() {
1564 let mut scope = Scope::new();
1567 scope.set_last_result(ExecResult::failure(42, "test error"));
1568
1569 let expr = Expr::VarRef(VarPath {
1570 segments: vec![VarSegment::Field("?".into())],
1571 });
1572 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1573 }
1574
1575 #[test]
1576 fn value_to_string_all_types() {
1577 assert_eq!(value_to_string(&Value::Null), "null");
1578 assert_eq!(value_to_string(&Value::Bool(true)), "true");
1579 assert_eq!(value_to_string(&Value::Int(42)), "42");
1580 assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1581 assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1582 }
1583
1584 #[test]
1587 fn eval_negative_int() {
1588 let mut scope = Scope::new();
1589 let expr = Expr::Literal(Value::Int(-42));
1590 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1591 }
1592
1593 #[test]
1594 fn eval_negative_float() {
1595 let mut scope = Scope::new();
1596 let expr = Expr::Literal(Value::Float(-3.14));
1597 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1598 }
1599
1600 #[test]
1601 fn eval_zero_values() {
1602 let mut scope = Scope::new();
1603 assert_eq!(
1604 eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1605 Ok(Value::Int(0))
1606 );
1607 assert_eq!(
1608 eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1609 Ok(Value::Float(0.0))
1610 );
1611 }
1612
1613 #[test]
1614 fn eval_interpolation_empty_var() {
1615 let mut scope = Scope::new();
1616 scope.set("EMPTY", Value::String("".into()));
1617
1618 let expr = Expr::Interpolated(vec![
1619 StringPart::Literal("prefix".into()),
1620 StringPart::Var(VarPath::simple("EMPTY")),
1621 StringPart::Literal("suffix".into()),
1622 ]);
1623 assert_eq!(
1624 eval_expr(&expr, &mut scope),
1625 Ok(Value::String("prefixsuffix".into()))
1626 );
1627 }
1628
1629 #[test]
1630 fn eval_chained_and() {
1631 let mut scope = Scope::new();
1632 let expr = Expr::BinaryOp {
1634 left: Box::new(Expr::BinaryOp {
1635 left: Box::new(Expr::Literal(Value::Bool(true))),
1636 op: BinaryOp::And,
1637 right: Box::new(Expr::Literal(Value::Bool(true))),
1638 }),
1639 op: BinaryOp::And,
1640 right: Box::new(Expr::Literal(Value::Int(42))),
1641 };
1642 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1643 }
1644
1645 #[test]
1646 fn eval_chained_or() {
1647 let mut scope = Scope::new();
1648 let expr = Expr::BinaryOp {
1650 left: Box::new(Expr::BinaryOp {
1651 left: Box::new(Expr::Literal(Value::Bool(false))),
1652 op: BinaryOp::Or,
1653 right: Box::new(Expr::Literal(Value::Bool(false))),
1654 }),
1655 op: BinaryOp::Or,
1656 right: Box::new(Expr::Literal(Value::Int(42))),
1657 };
1658 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1659 }
1660
1661 #[test]
1662 fn eval_mixed_and_or() {
1663 let mut scope = Scope::new();
1664 let expr = Expr::BinaryOp {
1667 left: Box::new(Expr::BinaryOp {
1668 left: Box::new(Expr::Literal(Value::Bool(true))),
1669 op: BinaryOp::Or,
1670 right: Box::new(Expr::Literal(Value::Bool(false))),
1671 }),
1672 op: BinaryOp::And,
1673 right: Box::new(Expr::Literal(Value::Bool(true))),
1674 };
1675 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1677 }
1678
1679 #[test]
1680 fn eval_interpolation_with_bool() {
1681 let mut scope = Scope::new();
1682 scope.set("FLAG", Value::Bool(true));
1683
1684 let expr = Expr::Interpolated(vec![
1685 StringPart::Literal("enabled: ".into()),
1686 StringPart::Var(VarPath::simple("FLAG")),
1687 ]);
1688 assert_eq!(
1689 eval_expr(&expr, &mut scope),
1690 Ok(Value::String("enabled: true".into()))
1691 );
1692 }
1693
1694 #[test]
1695 fn eval_interpolation_with_null() {
1696 let mut scope = Scope::new();
1697 scope.set("VAL", Value::Null);
1698
1699 let expr = Expr::Interpolated(vec![
1700 StringPart::Literal("value: ".into()),
1701 StringPart::Var(VarPath::simple("VAL")),
1702 ]);
1703 assert_eq!(
1704 eval_expr(&expr, &mut scope),
1705 Ok(Value::String("value: null".into()))
1706 );
1707 }
1708
1709 #[test]
1710 fn eval_format_path_simple() {
1711 let path = VarPath::simple("X");
1712 assert_eq!(format_path(&path), "${X}");
1713 }
1714
1715 #[test]
1716 fn eval_format_path_nested() {
1717 let path = VarPath {
1718 segments: vec![
1719 VarSegment::Field("X".into()),
1720 VarSegment::Field("field".into()),
1721 ],
1722 };
1723 assert_eq!(format_path(&path), "${X.field}");
1724 }
1725
1726 #[test]
1727 fn type_name_all_types() {
1728 assert_eq!(type_name(&Value::Null), "null");
1729 assert_eq!(type_name(&Value::Bool(true)), "bool");
1730 assert_eq!(type_name(&Value::Int(1)), "int");
1731 assert_eq!(type_name(&Value::Float(1.0)), "float");
1732 assert_eq!(type_name(&Value::String("".into())), "string");
1733 }
1734
1735 #[test]
1736 fn expand_tilde_home() {
1737 let home = "/home/session";
1739 assert_eq!(expand_tilde("~", Some(home)), home);
1740 assert_eq!(expand_tilde("~/foo", Some(home)), format!("{}/foo", home));
1741 assert_eq!(
1742 expand_tilde("~/foo/bar", Some(home)),
1743 format!("{}/foo/bar", home)
1744 );
1745 }
1746
1747 #[test]
1748 fn expand_tilde_hermetic_no_home_does_not_leak_host() {
1749 assert_eq!(expand_tilde("~", None), "~");
1752 assert_eq!(expand_tilde("~/foo", None), "~/foo");
1753 }
1754
1755 #[test]
1756 fn expand_tilde_passthrough() {
1757 assert_eq!(expand_tilde("/home/user", Some("/h")), "/home/user");
1759 assert_eq!(expand_tilde("foo~bar", Some("/h")), "foo~bar");
1760 assert_eq!(expand_tilde("", Some("/h")), "");
1761 }
1762
1763 #[test]
1764 #[cfg(all(unix, feature = "host"))]
1765 fn expand_tilde_user() {
1766 let expanded = expand_tilde("~root", None);
1769 assert!(
1771 expanded == "/root" || expanded == "/var/root",
1772 "expected /root or /var/root, got: {}",
1773 expanded
1774 );
1775
1776 let expanded_path = expand_tilde("~root/subdir", None);
1778 assert!(
1779 expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1780 "expected /root/subdir or /var/root/subdir, got: {}",
1781 expanded_path
1782 );
1783
1784 let nonexistent = expand_tilde("~nonexistent_user_12345", None);
1786 assert_eq!(nonexistent, "~nonexistent_user_12345");
1787 }
1788
1789 #[test]
1790 fn value_to_string_with_tilde_expansion() {
1791 let val = Value::String("~/test".into());
1793 assert_eq!(
1794 value_to_string_with_tilde(&val, Some("/home/session")),
1795 "/home/session/test"
1796 );
1797 }
1798
1799 #[test]
1800 fn eval_positional_param() {
1801 let mut scope = Scope::new();
1802 scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1803
1804 let expr = Expr::Positional(0);
1806 let result = eval_expr(&expr, &mut scope).unwrap();
1807 assert_eq!(result, Value::String("my_tool".into()));
1808
1809 let expr = Expr::Positional(1);
1811 let result = eval_expr(&expr, &mut scope).unwrap();
1812 assert_eq!(result, Value::String("hello".into()));
1813
1814 let expr = Expr::Positional(2);
1816 let result = eval_expr(&expr, &mut scope).unwrap();
1817 assert_eq!(result, Value::String("world".into()));
1818
1819 let expr = Expr::Positional(3);
1821 let result = eval_expr(&expr, &mut scope).unwrap();
1822 assert_eq!(result, Value::String("".into()));
1823 }
1824
1825 #[test]
1826 fn eval_all_args() {
1827 let mut scope = Scope::new();
1828 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1829
1830 let expr = Expr::AllArgs;
1831 let result = eval_expr(&expr, &mut scope).unwrap();
1832
1833 assert_eq!(result, Value::String("a b c".into()));
1835 }
1836
1837 #[test]
1838 fn eval_arg_count() {
1839 let mut scope = Scope::new();
1840 scope.set_positional("test", vec!["x".into(), "y".into()]);
1841
1842 let expr = Expr::ArgCount;
1843 let result = eval_expr(&expr, &mut scope).unwrap();
1844 assert_eq!(result, Value::Int(2));
1845 }
1846
1847 #[test]
1848 fn eval_arg_count_empty() {
1849 let mut scope = Scope::new();
1850
1851 let expr = Expr::ArgCount;
1852 let result = eval_expr(&expr, &mut scope).unwrap();
1853 assert_eq!(result, Value::Int(0));
1854 }
1855
1856 #[test]
1857 fn eval_var_length_string() {
1858 let mut scope = Scope::new();
1859 scope.set("NAME", Value::String("hello".into()));
1860
1861 let expr = Expr::VarLength(VarPath::simple("NAME"));
1862 let result = eval_expr(&expr, &mut scope).unwrap();
1863 assert_eq!(result, Value::Int(5));
1864 }
1865
1866 #[test]
1867 fn eval_var_length_empty_string() {
1868 let mut scope = Scope::new();
1869 scope.set("EMPTY", Value::String("".into()));
1870
1871 let expr = Expr::VarLength(VarPath::simple("EMPTY"));
1872 let result = eval_expr(&expr, &mut scope).unwrap();
1873 assert_eq!(result, Value::Int(0));
1874 }
1875
1876 #[test]
1877 fn eval_var_length_unset() {
1878 let mut scope = Scope::new();
1879
1880 let expr = Expr::VarLength(VarPath::simple("MISSING"));
1882 let result = eval_expr(&expr, &mut scope).unwrap();
1883 assert_eq!(result, Value::Int(0));
1884 }
1885
1886 #[test]
1887 fn eval_var_length_int() {
1888 let mut scope = Scope::new();
1889 scope.set("NUM", Value::Int(12345));
1890
1891 let expr = Expr::VarLength(VarPath::simple("NUM"));
1893 let result = eval_expr(&expr, &mut scope).unwrap();
1894 assert_eq!(result, Value::Int(5)); }
1896
1897 #[test]
1898 fn eval_var_with_default_set() {
1899 let mut scope = Scope::new();
1900 scope.set("NAME", Value::String("Alice".into()));
1901
1902 let expr = Expr::VarWithDefault {
1904 path: VarPath::simple("NAME"),
1905 default: vec![StringPart::Literal("default".into())],
1906 };
1907 let result = eval_expr(&expr, &mut scope).unwrap();
1908 assert_eq!(result, Value::String("Alice".into()));
1909 }
1910
1911 #[test]
1912 fn eval_var_with_default_unset() {
1913 let mut scope = Scope::new();
1914
1915 let expr = Expr::VarWithDefault {
1917 path: VarPath::simple("MISSING"),
1918 default: vec![StringPart::Literal("fallback".into())],
1919 };
1920 let result = eval_expr(&expr, &mut scope).unwrap();
1921 assert_eq!(result, Value::String("fallback".into()));
1922 }
1923
1924 #[test]
1925 fn eval_var_with_default_empty() {
1926 let mut scope = Scope::new();
1927 scope.set("EMPTY", Value::String("".into()));
1928
1929 let expr = Expr::VarWithDefault {
1931 path: VarPath::simple("EMPTY"),
1932 default: vec![StringPart::Literal("not empty".into())],
1933 };
1934 let result = eval_expr(&expr, &mut scope).unwrap();
1935 assert_eq!(result, Value::String("not empty".into()));
1936 }
1937
1938 #[test]
1939 fn eval_var_with_default_non_string() {
1940 let mut scope = Scope::new();
1941 scope.set("NUM", Value::Int(42));
1942
1943 let expr = Expr::VarWithDefault {
1945 path: VarPath::simple("NUM"),
1946 default: vec![StringPart::Literal("default".into())],
1947 };
1948 let result = eval_expr(&expr, &mut scope).unwrap();
1949 assert_eq!(result, Value::Int(42));
1950 }
1951
1952 #[test]
1953 fn eval_unset_variable_is_empty() {
1954 let mut scope = Scope::new();
1955 let parts = vec![
1956 StringPart::Literal("prefix:".into()),
1957 StringPart::Var(VarPath::simple("UNSET")),
1958 StringPart::Literal(":suffix".into()),
1959 ];
1960 let expr = Expr::Interpolated(parts);
1961 let result = eval_expr(&expr, &mut scope).unwrap();
1962 assert_eq!(result, Value::String("prefix::suffix".into()));
1963 }
1964
1965 #[test]
1966 fn eval_unset_variable_multiple() {
1967 let mut scope = Scope::new();
1968 scope.set("SET", Value::String("hello".into()));
1969 let parts = vec![
1970 StringPart::Var(VarPath::simple("UNSET1")),
1971 StringPart::Literal("-".into()),
1972 StringPart::Var(VarPath::simple("SET")),
1973 StringPart::Literal("-".into()),
1974 StringPart::Var(VarPath::simple("UNSET2")),
1975 ];
1976 let expr = Expr::Interpolated(parts);
1977 let result = eval_expr(&expr, &mut scope).unwrap();
1978 assert_eq!(result, Value::String("-hello-".into()));
1979 }
1980
1981 #[test]
1984 fn values_equal_scalars_still_work() {
1985 assert_eq!(
1986 values_equal(&Value::String("x".into()), &Value::String("x".into())),
1987 Ok(true)
1988 );
1989 assert_eq!(
1991 values_equal(&Value::String("42".into()), &Value::Int(42)),
1992 Ok(true)
1993 );
1994 }
1995
1996 #[test]
1997 fn values_equal_collection_vs_scalar_is_loud() {
1998 let list = Value::Json(serde_json::json!(["a", "b"]));
1999 let record = Value::Json(serde_json::json!({"k": 1}));
2000 assert!(
2001 matches!(values_equal(&list, &Value::String("banana".into())), Err(EvalError::Unsupported(_))),
2002 "list vs scalar must be a loud error, never silently false"
2003 );
2004 assert!(matches!(
2006 values_equal(&Value::String("x".into()), &record),
2007 Err(EvalError::Unsupported(_))
2008 ));
2009 }
2010
2011 #[test]
2012 fn values_equal_collection_vs_collection_is_structural() {
2013 let a = Value::Json(serde_json::json!({"a": 1, "b": 2}));
2015 let b = Value::Json(serde_json::json!({"b": 2, "a": 1}));
2016 assert_eq!(values_equal(&a, &b), Ok(true));
2017 }
2018
2019 #[test]
2022 fn values_equal_bytes_vs_bytes_still_works() {
2023 assert_eq!(
2025 values_equal(&Value::Bytes(vec![1, 2, 3]), &Value::Bytes(vec![1, 2, 3])),
2026 Ok(true)
2027 );
2028 assert_eq!(
2029 values_equal(&Value::Bytes(vec![1, 2, 3]), &Value::Bytes(vec![1, 2, 4])),
2030 Ok(false)
2031 );
2032 }
2033
2034 #[test]
2035 fn values_equal_bytes_vs_scalar_is_loud() {
2036 let bin = Value::Bytes(vec![0xff, 0x00]);
2040 assert!(matches!(
2041 values_equal(&bin, &Value::String("x".into())),
2042 Err(EvalError::Unsupported(_))
2043 ));
2044 assert!(matches!(
2045 values_equal(&Value::Int(1), &bin),
2046 Err(EvalError::Unsupported(_))
2047 ));
2048 }
2049
2050 #[test]
2051 fn eval_membership_bytes_needle_against_record_key_is_loud() {
2052 let record = Value::Json(serde_json::json!({"k": 1}));
2053 let bin = Value::Bytes(vec![0xff, 0x00]);
2054 assert!(matches!(
2055 eval_membership(&bin, &record),
2056 Err(EvalError::Unsupported(_))
2057 ));
2058 }
2059
2060 #[test]
2061 fn eval_membership_bytes_needle_against_list_is_not_a_match_not_an_abort() {
2062 let list = Value::Json(serde_json::json!(["a", "b"]));
2066 let bin = Value::Bytes(vec![0xff, 0x00]);
2067 assert_eq!(eval_membership(&bin, &list), Ok(false));
2068 }
2069
2070 #[test]
2071 fn value_length_of_bytes_is_byte_count() {
2072 assert_eq!(value_length(&Value::Bytes(vec![1, 2, 3])), 3);
2073 }
2074
2075 #[test]
2076 fn structured_export_error_flags_collections_passes_scalars() {
2077 let scalars = vec![
2079 ("A".to_string(), Value::String("x".into())),
2080 ("B".to_string(), Value::Int(1)),
2081 ];
2082 assert!(structured_export_error(&scalars).is_none());
2083 let with_record = vec![(
2085 "CFG".to_string(),
2086 Value::Json(serde_json::json!({"port": 8080})),
2087 )];
2088 let msg = structured_export_error(&with_record).expect("record must be refused");
2089 assert!(msg.contains("CFG") && msg.contains("tojson"), "got: {msg}");
2090 let with_list = vec![("XS".to_string(), Value::Json(serde_json::json!([1, 2])))];
2092 assert!(structured_export_error(&with_list).is_some());
2093 }
2094
2095 #[test]
2096 fn defaults_on_emptiness_matches_decision_a() {
2097 assert!(value_defaults_on_emptiness(&Value::Null));
2100 assert!(value_defaults_on_emptiness(&Value::Json(serde_json::Value::Null)));
2101 assert!(value_defaults_on_emptiness(&Value::String(String::new())));
2102 assert!(!value_defaults_on_emptiness(&Value::Bool(false)));
2103 assert!(!value_defaults_on_emptiness(&Value::Int(0)));
2104 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!([]))));
2105 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!({}))));
2106 assert!(!value_defaults_on_emptiness(&Value::String("x".into())));
2107 }
2108
2109 #[test]
2110 fn subscripted_length_and_default_resolve_the_path() {
2111 let mut scope = Scope::new();
2114 scope.set("u", Value::Json(serde_json::json!({"tags": ["a", "b"]})));
2115 let len = eval_expr(
2116 &Expr::VarLength(crate::parser::parse_varpath("${u[tags]}")),
2117 &mut scope,
2118 )
2119 .unwrap();
2120 assert_eq!(len, Value::Int(2));
2121
2122 scope.set("cfg", Value::Json(serde_json::json!({"port": 9000})));
2123 let val = eval_expr(
2125 &Expr::VarWithDefault {
2126 path: crate::parser::parse_varpath("${cfg[port]}"),
2127 default: vec![StringPart::Literal("8080".into())],
2128 },
2129 &mut scope,
2130 )
2131 .unwrap();
2132 assert_eq!(value_to_string(&val), "9000");
2133
2134 let missing = eval_expr(
2136 &Expr::VarWithDefault {
2137 path: crate::parser::parse_varpath("${cfg[nope]}"),
2138 default: vec![StringPart::Literal("8080".into())],
2139 },
2140 &mut scope,
2141 )
2142 .unwrap();
2143 assert_eq!(value_to_string(&missing), "8080");
2144
2145 let err = eval_expr(
2147 &Expr::VarWithDefault {
2148 path: crate::parser::parse_varpath("${cfg[0]}"),
2149 default: vec![StringPart::Literal("x".into())],
2150 },
2151 &mut scope,
2152 )
2153 .unwrap_err();
2154 assert!(matches!(err, EvalError::InvalidPath(_)), "got: {err}");
2155 }
2156}