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::NumericLiteral { value, .. } => self.eval_literal(value),
187 Expr::VarRef(path) => self.eval_var_ref(path),
188 Expr::Interpolated(parts) => self.eval_interpolated(parts),
189 Expr::HereDocBody { parts, strip_tabs } => {
190 let mut asm = HeredocAssembler::new(*strip_tabs);
193 for sp in parts {
194 match &sp.part {
195 StringPart::Literal(s) => asm.push_literal(s),
196 other => {
197 let value = self.eval_interpolated(std::slice::from_ref(other))?;
204 asm.push_interpolated(&value_to_text_sink(&value)?);
205 }
206 }
207 }
208 Ok(Value::String(asm.into_string()))
209 }
210 Expr::BinaryOp { left, op, right } => self.eval_binary_op(left, *op, right),
211 Expr::CommandSubst(_) => Err(EvalError::NoExecutor),
215 Expr::Test(test_expr) => self.eval_test(test_expr),
216 Expr::Positional(n) => self.eval_positional(*n),
217 Expr::AllArgs => self.eval_all_args(),
218 Expr::ArgCount => self.eval_arg_count(),
219 Expr::VarLength(path) => self.eval_var_length(path),
220 Expr::VarWithDefault { path, default } => self.eval_var_with_default(path, default),
221 Expr::Arithmetic(expr_str) => self.eval_arithmetic(expr_str),
222 Expr::Arith(expr_str) => self.eval_arith_cond(expr_str),
223 Expr::Command(cmd) => self.eval_command(cmd),
224 Expr::LastExitCode => self.eval_last_exit_code(),
225 Expr::CurrentPid => self.eval_current_pid(),
226 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
227 Expr::ListLiteral(elems) => self.eval_list_literal(elems),
228 Expr::RecordLiteral(entries) => self.eval_record_literal(entries),
229 }
230 }
231
232 fn eval_list_literal(&mut self, elems: &[ListElem]) -> EvalResult<Value> {
236 let mut out = Vec::with_capacity(elems.len());
237 for elem in elems {
238 match elem {
239 ListElem::Item(e) => {
240 let value = self.eval(e)?;
241 out.push(kaish_types::value_to_json(&value));
242 }
243 ListElem::Spread(e) => {
244 let value = self.eval(e)?;
245 match value {
246 Value::Json(serde_json::Value::Array(items)) => out.extend(items),
247 other => return Err(EvalError::Unsupported(spread_non_list_message(&other))),
248 }
249 }
250 }
251 }
252 Ok(Value::Json(serde_json::Value::Array(out)))
253 }
254
255 fn eval_record_literal(&mut self, entries: &[RecordEntry]) -> EvalResult<Value> {
260 let mut map = serde_json::Map::new();
261 for entry in entries {
262 let key = match &entry.key {
263 RecordKey::Bare(s) | RecordKey::Quoted(s) => s.clone(),
264 RecordKey::Interpolated(parts) => {
271 value_to_text_sink(&self.eval_interpolated(parts)?)?
272 }
273 };
274 let value = self.eval(&entry.value)?;
275 map.insert(key, kaish_types::value_to_json(&value));
276 }
277 Ok(Value::Json(serde_json::Value::Object(map)))
278 }
279
280 fn eval_last_exit_code(&self) -> EvalResult<Value> {
282 Ok(Value::Int(self.scope.last_result().code))
283 }
284
285 fn eval_current_pid(&self) -> EvalResult<Value> {
287 Ok(Value::Int(self.scope.pid() as i64))
288 }
289
290 fn eval_command(&mut self, cmd: &crate::ast::Command) -> EvalResult<Value> {
292 match cmd.name.as_str() {
295 "true" => Ok(Value::Bool(true)),
296 "false" => Ok(Value::Bool(false)),
297 _ => Err(EvalError::NoExecutor),
301 }
302 }
303
304 fn eval_arithmetic(&mut self, expr_str: &str) -> EvalResult<Value> {
306 arithmetic::eval_arithmetic(expr_str, self.scope)
307 .map(Value::Int)
308 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
309 }
310
311 fn eval_arith_cond(&mut self, expr_str: &str) -> EvalResult<Value> {
316 arithmetic::eval_arithmetic(expr_str, self.scope)
317 .map(|n| Value::Bool(n != 0))
318 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
319 }
320
321 fn eval_test(&mut self, test_expr: &TestExpr) -> EvalResult<Value> {
323 let result = match test_expr {
324 TestExpr::FileTest { .. } => {
325 return Err(EvalError::Unsupported(
332 "file tests must be resolved by the async evaluator".to_string(),
333 ));
334 }
335 TestExpr::StringTest { op, value } => match op {
336 StringTestOp::IsEmpty | StringTestOp::IsNonEmpty => {
337 let val = self.eval(value)?;
338 let symbol = match op {
342 StringTestOp::IsEmpty => "-z",
343 StringTestOp::IsNonEmpty => "-n",
344 StringTestOp::IsList | StringTestOp::IsRecord => unreachable!(),
345 };
346 if let Some(msg) = scalar_test_operand_error(symbol, &val) {
347 return Err(EvalError::Unsupported(msg));
348 }
349 let s = value_to_string(&val);
350 match op {
351 StringTestOp::IsEmpty => s.is_empty(),
352 StringTestOp::IsNonEmpty => !s.is_empty(),
353 StringTestOp::IsList | StringTestOp::IsRecord => unreachable!(),
354 }
355 }
356 StringTestOp::IsList | StringTestOp::IsRecord => {
363 let val = self.eval(value)?;
364 op.matches_shape(&val)
365 }
366 },
367 TestExpr::Comparison { left, op, right } => {
368 let left_val = self.eval(left)?;
369 let right_val = self.eval(right)?;
370
371 match op {
372 TestCmpOp::Eq => values_equal(&left_val, &right_val)?,
373 TestCmpOp::NotEq => !(values_equal(&left_val, &right_val)?),
374 TestCmpOp::Match => {
375 guard_scalar_test_operands(op, &left_val, &right_val)?;
377 match regex_match(&left_val, &right_val, false)? {
379 Value::Bool(b) => b,
380 _ => false,
381 }
382 }
383 TestCmpOp::NotMatch => {
384 guard_scalar_test_operands(op, &left_val, &right_val)?;
385 match regex_match(&left_val, &right_val, true)? {
387 Value::Bool(b) => b,
388 _ => true,
389 }
390 }
391 TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
392 guard_scalar_test_operands(op, &left_val, &right_val)?;
394 let ord = compare_values(&left_val, &right_val)?;
396 match op {
397 TestCmpOp::Gt => ord.is_gt(),
398 TestCmpOp::Lt => ord.is_lt(),
399 TestCmpOp::GtEq => ord.is_ge(),
400 TestCmpOp::LtEq => ord.is_le(),
401 _ => unreachable!(),
402 }
403 }
404 TestCmpOp::NumEq
405 | TestCmpOp::NumNotEq
406 | TestCmpOp::NumGt
407 | TestCmpOp::NumLt
408 | TestCmpOp::NumGtEq
409 | TestCmpOp::NumLtEq => {
410 guard_scalar_test_operands(op, &left_val, &right_val)?;
412 let ord = numeric_compare(&left_val, &right_val)?;
415 match op {
416 TestCmpOp::NumEq => ord.is_eq(),
417 TestCmpOp::NumNotEq => !ord.is_eq(),
418 TestCmpOp::NumGt => ord.is_gt(),
419 TestCmpOp::NumLt => ord.is_lt(),
420 TestCmpOp::NumGtEq => ord.is_ge(),
421 TestCmpOp::NumLtEq => ord.is_le(),
422 _ => unreachable!(),
423 }
424 }
425 }
426 }
427 TestExpr::And { left, right } => {
428 let left_result = self.eval_test(left)?;
430 if !value_to_bool(&left_result) {
431 false } else {
433 value_to_bool(&self.eval_test(right)?)
434 }
435 }
436 TestExpr::Or { left, right } => {
437 let left_result = self.eval_test(left)?;
439 if value_to_bool(&left_result) {
440 true } else {
442 value_to_bool(&self.eval_test(right)?)
443 }
444 }
445 TestExpr::Not { expr } => {
446 let result = self.eval_test(expr)?;
447 !value_to_bool(&result)
448 }
449 TestExpr::In { left, right } => {
450 let left_val = self.eval(left)?;
451 let right_val = self.eval(right)?;
452 eval_membership(&left_val, &right_val)?
453 }
454 TestExpr::NotIn { left, right } => {
455 let left_val = self.eval(left)?;
456 let right_val = self.eval(right)?;
457 !eval_membership(&left_val, &right_val)?
458 }
459 };
460 Ok(Value::Bool(result))
461 }
462
463 fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
465 Ok(value.clone())
466 }
467
468 fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
470 match self.scope.resolve_path(path) {
471 Ok(v) => Ok(v),
472 Err(super::scope::PathError::UndefinedRoot(_)) => {
474 Err(EvalError::InvalidPath(format_path(path)))
475 }
476 Err(super::scope::PathError::Absence(msg))
479 | Err(super::scope::PathError::Shape(msg)) => Err(EvalError::InvalidPath(msg)),
480 }
481 }
482
483 fn eval_positional(&self, n: usize) -> EvalResult<Value> {
485 match self.scope.get_positional(n) {
486 Some(s) => Ok(Value::String(s.to_string())),
487 None => Ok(Value::String(String::new())), }
489 }
490
491 fn eval_all_args(&self) -> EvalResult<Value> {
495 let args = self.scope.all_args();
496 Ok(Value::String(args.join(" ")))
497 }
498
499 fn eval_arg_count(&self) -> EvalResult<Value> {
501 Ok(Value::Int(self.scope.arg_count() as i64))
502 }
503
504 fn eval_var_length(&self, path: &VarPath) -> EvalResult<Value> {
506 resolve_length(self.scope, path)
507 .map(Value::Int)
508 .map_err(EvalError::InvalidPath)
509 }
510
511 fn eval_var_with_default(&mut self, path: &VarPath, default: &[StringPart]) -> EvalResult<Value> {
515 match resolve_default(self.scope, path).map_err(EvalError::InvalidPath)? {
516 Some(value) => Ok(value),
517 None => self.eval_interpolated(default),
518 }
519 }
520
521 fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
523 let mut result = String::new();
524 for part in parts {
525 match part {
526 StringPart::Literal(s) => result.push_str(s),
527 StringPart::Var(path) => {
528 match self.scope.resolve_path(path) {
529 Ok(value) => result.push_str(&value_to_text_sink(&value)?),
531 Err(super::scope::PathError::UndefinedRoot(_)) => {}
533 Err(super::scope::PathError::Absence(msg))
536 | Err(super::scope::PathError::Shape(msg)) => {
537 return Err(EvalError::InvalidPath(msg))
538 }
539 }
540 }
541 StringPart::VarWithDefault { path, default } => {
542 let value = self.eval_var_with_default(path, default)?;
543 result.push_str(&value_to_text_sink(&value)?);
544 }
545 StringPart::VarLength(path) => {
546 let value = self.eval_var_length(path)?;
547 result.push_str(&value_to_text_sink(&value)?);
548 }
549 StringPart::Positional(n) => {
550 let value = self.eval_positional(*n)?;
551 result.push_str(&value_to_text_sink(&value)?);
552 }
553 StringPart::AllArgs => {
554 let value = self.eval_all_args()?;
555 result.push_str(&value_to_text_sink(&value)?);
556 }
557 StringPart::ArgCount => {
558 let value = self.eval_arg_count()?;
559 result.push_str(&value_to_text_sink(&value)?);
560 }
561 StringPart::Arithmetic(expr) => {
562 let value = self.eval_arithmetic_string(expr)?;
564 result.push_str(&value_to_text_sink(&value)?);
565 }
566 StringPart::CommandSubst(_) => {
567 return Err(EvalError::NoExecutor);
573 }
574 StringPart::LastExitCode => {
575 result.push_str(&self.scope.last_result().code.to_string());
576 }
577 StringPart::CurrentPid => {
578 result.push_str(&self.scope.pid().to_string());
579 }
580 }
581 }
582 Ok(Value::String(result))
583 }
584
585 fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
587 arithmetic::eval_arithmetic(expr, self.scope)
589 .map(Value::Int)
590 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
591 }
592
593 fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
597 match op {
598 BinaryOp::And => {
599 let left_val = self.eval(left)?;
600 if !is_truthy(&left_val) {
601 return Ok(left_val);
602 }
603 self.eval(right)
604 }
605 BinaryOp::Or => {
606 let left_val = self.eval(left)?;
607 if is_truthy(&left_val) {
608 return Ok(left_val);
609 }
610 self.eval(right)
611 }
612 }
613 }
614
615}
616
617pub fn value_to_exit_code(value: &Value) -> anyhow::Result<i64> {
623 match value {
624 Value::Int(n) => Ok(*n),
625 Value::Bool(b) => Ok(if *b { 0 } else { 1 }),
626 Value::Float(f) => Ok(*f as i64),
627 Value::String(s) => {
628 let trimmed = s.trim();
629 trimmed.parse::<i64>().map_err(|_| {
630 if is_i64_overflow_shape(trimmed) {
631 anyhow::anyhow!("`{trimmed}`, which {}", crate::lexer::INTEGER_OUT_OF_RANGE)
632 } else {
633 anyhow::anyhow!("numeric argument required: {:?}", s)
634 }
635 })
636 }
637 Value::Null | Value::Json(_) | Value::Bytes(_) => {
638 anyhow::bail!("numeric argument required (got {:?})", value)
639 }
640 }
641}
642
643pub(crate) fn is_i64_overflow_shape(t: &str) -> bool {
648 let digits = t.strip_prefix('-').unwrap_or(t);
649 !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
650}
651
652pub fn value_length(value: &Value) -> i64 {
665 match value {
666 Value::Json(serde_json::Value::Array(a)) => a.len() as i64,
667 Value::Json(serde_json::Value::Object(o)) => o.len() as i64,
668 Value::Bytes(b) => b.len() as i64,
672 Value::String(s) => s.chars().count() as i64,
675 other => value_to_string(other).chars().count() as i64,
676 }
677}
678
679pub fn value_defaults_on_emptiness(value: &Value) -> bool {
686 match value {
687 Value::Null | Value::Json(serde_json::Value::Null) => true,
688 Value::String(s) => s.is_empty(),
689 _ => false,
690 }
691}
692
693pub fn resolve_length(scope: &Scope, path: &VarPath) -> Result<i64, String> {
700 match scope.resolve_path(path) {
701 Ok(value) => Ok(value_length(&value)),
702 Err(super::scope::PathError::UndefinedRoot(_)) if path.segments.len() <= 1 => Ok(0),
703 Err(super::scope::PathError::UndefinedRoot(_)) => {
704 Err(format!("{}: undefined variable", format_path(path)))
705 }
706 Err(super::scope::PathError::Absence(msg)) | Err(super::scope::PathError::Shape(msg)) => {
707 Err(msg)
708 }
709 }
710}
711
712pub fn resolve_default(scope: &Scope, path: &VarPath) -> Result<Option<Value>, String> {
718 match scope.resolve_path(path) {
719 Ok(value) if value_defaults_on_emptiness(&value) => Ok(None),
720 Ok(value) => Ok(Some(value)),
721 Err(super::scope::PathError::UndefinedRoot(_))
722 | Err(super::scope::PathError::Absence(_)) => Ok(None),
723 Err(super::scope::PathError::Shape(msg)) => Err(msg),
724 }
725}
726
727pub fn structured_export_error(vars: &[(String, Value)]) -> Option<String> {
733 for (name, value) in vars {
734 if let Value::Json(j) = value {
735 if matches!(j, serde_json::Value::Array(_) | serde_json::Value::Object(_)) {
736 let kind = if j.is_array() { "list" } else { "record" };
737 return Some(format!(
738 "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})`"
739 ));
740 }
741 }
742 }
743 None
744}
745
746pub fn is_collection(value: &Value) -> bool {
751 matches!(
752 value,
753 Value::Json(serde_json::Value::Array(_)) | Value::Json(serde_json::Value::Object(_))
754 )
755}
756
757fn collection_kind(value: &Value) -> &'static str {
760 match value {
761 Value::Json(serde_json::Value::Array(_)) => "list",
762 Value::Json(serde_json::Value::Object(_)) => "record",
763 _ => "collection",
764 }
765}
766
767pub fn structured_boundary_error(sink: &str, value: &Value) -> Option<String> {
777 if is_collection(value) {
778 let kind = collection_kind(value);
779 Some(format!(
780 "cannot use a {kind} as {sink} — serialize it explicitly first, e.g. `cmd $(tojson $x)`"
781 ))
782 } else {
783 None
784 }
785}
786
787pub fn scalar_test_operand_error(op_symbol: &str, value: &Value) -> Option<String> {
796 if is_collection(value) {
797 let kind = collection_kind(value);
798 Some(format!(
799 "`{op_symbol}` needs a scalar; got a {kind} — use `${{#x}}` for length, \
800 `-list`/`-record` to test shape, or `in` for membership"
801 ))
802 } else {
803 None
804 }
805}
806
807pub fn value_to_string(value: &Value) -> String {
809 match value {
810 Value::Null => "null".to_string(),
811 Value::Bool(b) => b.to_string(),
812 Value::Int(i) => i.to_string(),
813 Value::Float(f) => f.to_string(),
814 Value::String(s) => s.clone(),
815 Value::Json(json) => json.to_string(),
816 Value::Bytes(b) => format!("[binary: {} bytes]", b.len()),
822 }
823}
824
825pub fn value_to_text_sink(value: &Value) -> EvalResult<String> {
844 value_to_text_sink_named(value, "text")
845}
846
847pub fn value_to_text_sink_named(value: &Value, sink: &str) -> EvalResult<String> {
856 match value {
857 Value::Bytes(b) => match std::str::from_utf8(b) {
858 Ok(s) => Ok(s.to_string()),
859 Err(_) => Err(EvalError::Unsupported(format!(
860 "binary data ({} bytes) cannot be used as {sink} — decode it \
861 (base64/xxd) or redirect to a file",
862 b.len()
863 ))),
864 },
865 other => Ok(value_to_string(other)),
866 }
867}
868
869pub fn values_to_text_sink_named(values: &[Value], sink: &str) -> EvalResult<Vec<String>> {
873 values.iter().map(|v| value_to_text_sink_named(v, sink)).collect()
874}
875
876pub fn value_to_bool(value: &Value) -> bool {
886 match value {
887 Value::Null => false,
888 Value::Bool(b) => *b,
889 Value::Int(i) => *i != 0,
890 Value::Float(f) => *f != 0.0,
891 Value::String(s) => !s.is_empty(),
892 Value::Json(json) => match json {
893 serde_json::Value::Null => false,
894 serde_json::Value::Array(arr) => !arr.is_empty(),
895 serde_json::Value::Object(obj) => !obj.is_empty(),
896 serde_json::Value::Bool(b) => *b,
897 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
898 serde_json::Value::String(s) => !s.is_empty(),
899 },
900 Value::Bytes(b) => !b.is_empty(), }
902}
903
904pub fn expand_tilde(s: &str, home: Option<&str>) -> String {
918 if s == "~" {
919 home.map(|h| h.to_string()).unwrap_or_else(|| "~".to_string())
920 } else if s.starts_with("~/") {
921 match home {
922 Some(home) => format!("{}{}", home, &s[1..]),
923 None => s.to_string(),
924 }
925 } else if s.starts_with('~') {
926 expand_tilde_user(s)
928 } else {
929 s.to_string()
930 }
931}
932
933#[cfg(all(unix, feature = "host"))]
938fn expand_tilde_user(s: &str) -> String {
939 let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
941 (&s[1..slash_pos + 1], &s[slash_pos + 1..])
942 } else {
943 (&s[1..], "")
944 };
945
946 if username.is_empty() {
947 return s.to_string();
948 }
949
950 let passwd = match std::fs::read_to_string("/etc/passwd") {
953 Ok(content) => content,
954 Err(_) => return s.to_string(),
955 };
956
957 for line in passwd.lines() {
958 let fields: Vec<&str> = line.split(':').collect();
959 if fields.len() >= 6 && fields[0] == username {
960 let home_dir = fields[5];
961 return if rest.is_empty() {
962 home_dir.to_string()
963 } else {
964 format!("{}{}", home_dir, rest)
965 };
966 }
967 }
968
969 s.to_string()
971}
972
973#[cfg(not(all(unix, feature = "host")))]
974fn expand_tilde_user(s: &str) -> String {
975 s.to_string()
978}
979
980pub fn value_to_string_with_tilde(value: &Value, home: Option<&str>) -> String {
985 match value {
986 Value::String(s) if s.starts_with('~') => expand_tilde(s, home),
987 _ => value_to_string(value),
988 }
989}
990
991pub(crate) fn format_path(path: &VarPath) -> String {
996 use crate::ast::VarSegment;
997 let mut result = String::from("${");
998 for (i, seg) in path.segments.iter().enumerate() {
999 match seg {
1000 VarSegment::Field(name) => {
1001 if i > 0 {
1002 result.push('.');
1003 }
1004 result.push_str(name);
1005 }
1006 VarSegment::Index(idx) => result.push_str(&format!("[{idx}]")),
1007 VarSegment::Key(k) => result.push_str(&format!("[{k}]")),
1008 VarSegment::Dynamic(v) => result.push_str(&format!("[${v}]")),
1009 VarSegment::Slice(a, b) => {
1010 let s = a.map(|n| n.to_string()).unwrap_or_default();
1011 let e = b.map(|n| n.to_string()).unwrap_or_default();
1012 result.push_str(&format!("[{s}:{e}]"));
1013 }
1014 }
1015 }
1016 result.push('}');
1017 result
1018}
1019
1020fn is_truthy(value: &Value) -> bool {
1030 value_to_bool(value)
1032}
1033
1034pub fn values_equal(left: &Value, right: &Value) -> EvalResult<bool> {
1045 match (left, right) {
1046 (Value::Null, Value::Null) => Ok(true),
1047 (Value::Bool(a), Value::Bool(b)) => Ok(a == b),
1048 (Value::Int(a), Value::Int(b)) => Ok(a == b),
1049 (Value::Float(a), Value::Float(b)) => Ok((a - b).abs() < f64::EPSILON),
1050 (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
1051 Ok((*a as f64 - b).abs() < f64::EPSILON)
1052 }
1053 (Value::String(a), Value::String(b)) => Ok(a == b),
1054 (Value::Json(a), Value::Json(b)) => Ok(a == b),
1055 (Value::Bytes(a), Value::Bytes(b)) => Ok(a == b),
1056 (Value::Json(j), other) | (other, Value::Json(j))
1062 if matches!(j, serde_json::Value::Array(_) | serde_json::Value::Object(_)) =>
1063 {
1064 let kind = if j.is_array() { "list" } else { "record" };
1065 Err(EvalError::Unsupported(format!(
1066 "cannot compare a {kind} to a {other_kind} with ==/!= — test membership with `[[ x in $coll ]]`, or compare structures with `jq`",
1067 other_kind = type_name(other),
1068 )))
1069 }
1070 (Value::Bytes(b), other) | (other, Value::Bytes(b)) => Err(EvalError::Unsupported(format!(
1076 "binary data ({} bytes) cannot be used as an ==/!= operand against a {} — decode it \
1077 first (base64/xxd), or compare two binary values directly",
1078 b.len(),
1079 type_name(other),
1080 ))),
1081 _ => Ok(value_to_string(left) == value_to_string(right)),
1084 }
1085}
1086
1087fn element_matches(needle: &Value, element: &Value) -> bool {
1096 match (needle, element) {
1097 (Value::Json(a), Value::Json(b)) => a == b,
1098 (Value::Json(_), _) | (_, Value::Json(_)) => false,
1099 _ => values_equal(needle, element).unwrap_or(false),
1106 }
1107}
1108
1109fn eval_membership(needle: &Value, haystack: &Value) -> EvalResult<bool> {
1119 match haystack {
1120 Value::Json(serde_json::Value::Array(items)) => {
1121 for item in items {
1122 let element = json_to_value_no_envelope(item.clone());
1123 if element_matches(needle, &element) {
1124 return Ok(true);
1125 }
1126 }
1127 Ok(false)
1128 }
1129 Value::Json(serde_json::Value::Object(map)) => {
1130 if let Value::Bytes(b) = needle {
1135 return Err(EvalError::Unsupported(format!(
1136 "binary data ({} bytes) cannot be used as a record key for `in` — \
1137 decode it first (base64/xxd)",
1138 b.len()
1139 )));
1140 }
1141 Ok(map.contains_key(&value_to_string(needle)))
1142 }
1143 other => Err(EvalError::Unsupported(format!(
1144 "`in` requires a list or record on the right-hand side, got {} — substring tests use `=~`, glob (`[[ $s == *sub* ]]`), or `case`",
1145 type_name(other),
1146 ))),
1147 }
1148}
1149
1150fn cmp_op_symbol(op: &TestCmpOp) -> &'static str {
1153 match op {
1154 TestCmpOp::Eq => "==",
1155 TestCmpOp::NotEq => "!=",
1156 TestCmpOp::Match => "=~",
1157 TestCmpOp::NotMatch => "!~",
1158 TestCmpOp::Gt => ">",
1159 TestCmpOp::Lt => "<",
1160 TestCmpOp::GtEq => ">=",
1161 TestCmpOp::LtEq => "<=",
1162 TestCmpOp::NumEq => "-eq",
1163 TestCmpOp::NumNotEq => "-ne",
1164 TestCmpOp::NumGt => "-gt",
1165 TestCmpOp::NumLt => "-lt",
1166 TestCmpOp::NumGtEq => "-ge",
1167 TestCmpOp::NumLtEq => "-le",
1168 }
1169}
1170
1171fn guard_scalar_test_operands(op: &TestCmpOp, left: &Value, right: &Value) -> EvalResult<()> {
1175 let symbol = cmp_op_symbol(op);
1176 if let Some(msg) = scalar_test_operand_error(symbol, left) {
1177 return Err(EvalError::Unsupported(msg));
1178 }
1179 if let Some(msg) = scalar_test_operand_error(symbol, right) {
1180 return Err(EvalError::Unsupported(msg));
1181 }
1182 Ok(())
1183}
1184
1185fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1187 match (left, right) {
1188 (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
1189 (Value::Float(a), Value::Float(b)) => {
1190 a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1191 }
1192 (Value::Int(a), Value::Float(b)) => {
1193 (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1194 }
1195 (Value::Float(a), Value::Int(b)) => {
1196 a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1197 }
1198 (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
1199 _ => Err(EvalError::TypeError {
1200 expected: "comparable types (numbers or strings)",
1201 got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
1202 }),
1203 }
1204}
1205
1206enum Num {
1208 Int(i64),
1209 Float(f64),
1210}
1211
1212fn value_to_num(value: &Value) -> EvalResult<Num> {
1220 match value {
1221 Value::Int(n) => Ok(Num::Int(*n)),
1222 Value::Float(f) => Ok(Num::Float(*f)),
1223 Value::String(s) => {
1224 let t = s.trim();
1225 if let Some(decimal) = arithmetic::leading_zero_decimal(t) {
1227 return Err(EvalError::TypeError {
1228 expected: "a number",
1229 got: format!(
1230 "`{t}`, which is text (leading zero) — kaish reads no octal; write \
1231 `{decimal}` for the decimal value"
1232 ),
1233 });
1234 }
1235 if let Ok(n) = t.parse::<i64>() {
1236 return Ok(Num::Int(n));
1237 }
1238 let looks_like_float = t.contains(['.', 'e', 'E']);
1244 if looks_like_float
1245 && let Ok(f) = t.parse::<f64>()
1246 {
1247 if !f.is_finite() {
1251 return Err(EvalError::TypeError {
1252 expected: "a number",
1253 got: format!("`{t}`, which is outside the 64-bit float range"),
1254 });
1255 }
1256 return Ok(Num::Float(f));
1257 }
1258 if is_i64_overflow_shape(t) {
1259 Err(EvalError::TypeError {
1260 expected: "a number",
1261 got: format!("`{t}`, which {}", crate::lexer::INTEGER_OUT_OF_RANGE),
1262 })
1263 } else {
1264 Err(EvalError::TypeError {
1265 expected: "numeric operand",
1266 got: format!("non-numeric string {:?}", s),
1267 })
1268 }
1269 }
1270 _ => Err(EvalError::TypeError {
1271 expected: "numeric operand",
1272 got: type_name(value).to_string(),
1273 }),
1274 }
1275}
1276
1277pub fn numeric_compare(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1282 let l = value_to_num(left)?;
1283 let r = value_to_num(right)?;
1284 match (l, r) {
1285 (Num::Int(a), Num::Int(b)) => Ok(a.cmp(&b)),
1286 (Num::Float(a), Num::Float(b)) => a
1287 .partial_cmp(&b)
1288 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1289 (Num::Int(a), Num::Float(b)) => (a as f64)
1290 .partial_cmp(&b)
1291 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1292 (Num::Float(a), Num::Int(b)) => a
1293 .partial_cmp(&(b as f64))
1294 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1295 }
1296}
1297
1298fn type_name(value: &Value) -> &'static str {
1300 match value {
1301 Value::Null => "null",
1302 Value::Bool(_) => "bool",
1303 Value::Int(_) => "int",
1304 Value::Float(_) => "float",
1305 Value::String(_) => "string",
1306 Value::Json(_) => "json",
1307 Value::Bytes(_) => "bytes",
1308 }
1309}
1310
1311fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
1316 let text = match left {
1317 Value::String(s) => s.as_str(),
1318 _ => {
1319 return Err(EvalError::TypeError {
1320 expected: "string",
1321 got: type_name(left).to_string(),
1322 })
1323 }
1324 };
1325
1326 let pattern = match right {
1327 Value::String(s) => s.as_str(),
1328 _ => {
1329 return Err(EvalError::TypeError {
1330 expected: "string (regex pattern)",
1331 got: type_name(right).to_string(),
1332 })
1333 }
1334 };
1335
1336 let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
1337 let matches = re.is_match(text);
1338
1339 Ok(Value::Bool(if negate { !matches } else { matches }))
1340}
1341
1342pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
1349 let mut evaluator = Evaluator::new(scope);
1350 evaluator.eval(expr)
1351}
1352
1353#[cfg(test)]
1354#[allow(clippy::approx_constant)]
1355mod tests {
1356 use super::*;
1357 use crate::ast::{Stmt, VarSegment};
1358 use super::super::result::ExecResult;
1359
1360 fn var_expr(name: &str) -> Expr {
1362 Expr::VarRef(VarPath::simple(name))
1363 }
1364
1365 #[test]
1366 fn eval_literal_int() {
1367 let mut scope = Scope::new();
1368 let expr = Expr::Literal(Value::Int(42));
1369 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1370 }
1371
1372 #[test]
1373 fn eval_literal_string() {
1374 let mut scope = Scope::new();
1375 let expr = Expr::Literal(Value::String("hello".into()));
1376 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
1377 }
1378
1379 #[test]
1380 fn eval_literal_bool() {
1381 let mut scope = Scope::new();
1382 assert_eq!(
1383 eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
1384 Ok(Value::Bool(true))
1385 );
1386 }
1387
1388 #[test]
1389 fn eval_literal_null() {
1390 let mut scope = Scope::new();
1391 assert_eq!(
1392 eval_expr(&Expr::Literal(Value::Null), &mut scope),
1393 Ok(Value::Null)
1394 );
1395 }
1396
1397 #[test]
1398 fn eval_literal_float() {
1399 let mut scope = Scope::new();
1400 let expr = Expr::Literal(Value::Float(3.14));
1401 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
1402 }
1403
1404 #[test]
1405 fn eval_variable_ref() {
1406 let mut scope = Scope::new();
1407 scope.set("X", Value::Int(100));
1408 assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
1409 }
1410
1411 #[test]
1412 fn eval_undefined_variable() {
1413 let mut scope = Scope::new();
1414 let result = eval_expr(&var_expr("MISSING"), &mut scope);
1415 assert!(matches!(result, Err(EvalError::InvalidPath(_))));
1416 }
1417
1418 #[test]
1419 fn eval_interpolated_string() {
1420 let mut scope = Scope::new();
1421 scope.set("NAME", Value::String("World".into()));
1422
1423 let expr = Expr::Interpolated(vec![
1424 StringPart::Literal("Hello, ".into()),
1425 StringPart::Var(VarPath::simple("NAME")),
1426 StringPart::Literal("!".into()),
1427 ]);
1428 assert_eq!(
1429 eval_expr(&expr, &mut scope),
1430 Ok(Value::String("Hello, World!".into()))
1431 );
1432 }
1433
1434 #[test]
1446 fn eval_heredoc_body_binary_var_is_loud() {
1447 let mut scope = Scope::new();
1448 scope.set("B", Value::Bytes(vec![0xff, 0x00, 0xfe]));
1449
1450 let expr = Expr::HereDocBody {
1451 parts: vec![
1452 crate::ast::SpannedPart {
1453 part: StringPart::Literal("before ".into()),
1454 offset: 0,
1455 len: 0,
1456 },
1457 crate::ast::SpannedPart {
1458 part: StringPart::Var(VarPath::simple("B")),
1459 offset: 0,
1460 len: 0,
1461 },
1462 ],
1463 strip_tabs: false,
1464 };
1465 let err = eval_expr(&expr, &mut scope).expect_err("binary in a heredoc body must be loud");
1466 assert!(
1467 matches!(err, EvalError::Unsupported(ref msg) if msg.contains("cannot be used as")),
1468 "got {err:?}"
1469 );
1470 }
1471
1472 #[test]
1473 fn eval_heredoc_body_text_var_is_unaffected() {
1474 let mut scope = Scope::new();
1475 scope.set("NAME", Value::String("World".into()));
1476
1477 let expr = Expr::HereDocBody {
1478 parts: vec![
1479 crate::ast::SpannedPart {
1480 part: StringPart::Literal("Hello, ".into()),
1481 offset: 0,
1482 len: 0,
1483 },
1484 crate::ast::SpannedPart {
1485 part: StringPart::Var(VarPath::simple("NAME")),
1486 offset: 0,
1487 len: 0,
1488 },
1489 ],
1490 strip_tabs: false,
1491 };
1492 assert_eq!(
1493 eval_expr(&expr, &mut scope),
1494 Ok(Value::String("Hello, World".into()))
1495 );
1496 }
1497
1498 #[test]
1499 fn eval_record_literal_interpolated_key_binary_var_is_loud() {
1500 let mut scope = Scope::new();
1501 scope.set("B", Value::Bytes(vec![0xff, 0x00, 0xfe]));
1502
1503 let expr = Expr::RecordLiteral(vec![RecordEntry {
1504 key: RecordKey::Interpolated(vec![StringPart::Var(VarPath::simple("B"))]),
1505 value: Expr::Literal(Value::Int(1)),
1506 }]);
1507 let err = eval_expr(&expr, &mut scope)
1508 .expect_err("a binary record key must be loud, not a `[binary: N bytes]` key");
1509 assert!(
1510 matches!(err, EvalError::Unsupported(ref msg) if msg.contains("cannot be used as")),
1511 "got {err:?}"
1512 );
1513 }
1514
1515 #[test]
1516 fn eval_record_literal_interpolated_key_text_var_is_unaffected() {
1517 let mut scope = Scope::new();
1518 scope.set("K", Value::String("port".into()));
1519
1520 let expr = Expr::RecordLiteral(vec![RecordEntry {
1521 key: RecordKey::Interpolated(vec![StringPart::Var(VarPath::simple("K"))]),
1522 value: Expr::Literal(Value::Int(8080)),
1523 }]);
1524 assert_eq!(
1525 eval_expr(&expr, &mut scope),
1526 Ok(Value::Json(serde_json::json!({"port": 8080})))
1527 );
1528 }
1529
1530 #[test]
1531 fn eval_interpolated_with_number() {
1532 let mut scope = Scope::new();
1533 scope.set("COUNT", Value::Int(42));
1534
1535 let expr = Expr::Interpolated(vec![
1536 StringPart::Literal("Count: ".into()),
1537 StringPart::Var(VarPath::simple("COUNT")),
1538 ]);
1539 assert_eq!(
1540 eval_expr(&expr, &mut scope),
1541 Ok(Value::String("Count: 42".into()))
1542 );
1543 }
1544
1545 #[test]
1546 fn eval_and_short_circuit_true() {
1547 let mut scope = Scope::new();
1548 let expr = Expr::BinaryOp {
1549 left: Box::new(Expr::Literal(Value::Bool(true))),
1550 op: BinaryOp::And,
1551 right: Box::new(Expr::Literal(Value::Int(42))),
1552 };
1553 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1555 }
1556
1557 #[test]
1558 fn eval_and_short_circuit_false() {
1559 let mut scope = Scope::new();
1560 let expr = Expr::BinaryOp {
1561 left: Box::new(Expr::Literal(Value::Bool(false))),
1562 op: BinaryOp::And,
1563 right: Box::new(Expr::Literal(Value::Int(42))),
1564 };
1565 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
1567 }
1568
1569 #[test]
1570 fn eval_or_short_circuit_true() {
1571 let mut scope = Scope::new();
1572 let expr = Expr::BinaryOp {
1573 left: Box::new(Expr::Literal(Value::Bool(true))),
1574 op: BinaryOp::Or,
1575 right: Box::new(Expr::Literal(Value::Int(42))),
1576 };
1577 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1579 }
1580
1581 #[test]
1582 fn eval_or_short_circuit_false() {
1583 let mut scope = Scope::new();
1584 let expr = Expr::BinaryOp {
1585 left: Box::new(Expr::Literal(Value::Bool(false))),
1586 op: BinaryOp::Or,
1587 right: Box::new(Expr::Literal(Value::Int(42))),
1588 };
1589 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1591 }
1592
1593 #[test]
1594 fn is_truthy_values() {
1595 assert!(!is_truthy(&Value::Null));
1596 assert!(!is_truthy(&Value::Bool(false)));
1597 assert!(is_truthy(&Value::Bool(true)));
1598 assert!(!is_truthy(&Value::Int(0)));
1599 assert!(is_truthy(&Value::Int(1)));
1600 assert!(is_truthy(&Value::Int(-1)));
1601 assert!(!is_truthy(&Value::Float(0.0)));
1602 assert!(is_truthy(&Value::Float(0.1)));
1603 assert!(!is_truthy(&Value::String("".into())));
1604 assert!(is_truthy(&Value::String("x".into())));
1605 }
1606
1607 #[test]
1608 fn sync_command_subst_is_loud_not_silent() {
1609 use crate::ast::Command;
1613
1614 let mut scope = Scope::new();
1615 let expr = Expr::CommandSubst(vec![Stmt::Command(Command {
1616 name: "echo".into(),
1617 args: vec![],
1618 redirects: vec![],
1619 })]);
1620
1621 assert!(matches!(
1622 eval_expr(&expr, &mut scope),
1623 Err(EvalError::NoExecutor)
1624 ));
1625 }
1626
1627 #[test]
1628 fn eval_last_result_bare() {
1629 let mut scope = Scope::new();
1632 scope.set_last_result(ExecResult::failure(42, "test error"));
1633
1634 let expr = Expr::VarRef(VarPath {
1635 segments: vec![VarSegment::Field("?".into())],
1636 });
1637 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1638 }
1639
1640 #[test]
1641 fn value_to_string_all_types() {
1642 assert_eq!(value_to_string(&Value::Null), "null");
1643 assert_eq!(value_to_string(&Value::Bool(true)), "true");
1644 assert_eq!(value_to_string(&Value::Int(42)), "42");
1645 assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1646 assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1647 }
1648
1649 #[test]
1652 fn eval_negative_int() {
1653 let mut scope = Scope::new();
1654 let expr = Expr::Literal(Value::Int(-42));
1655 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1656 }
1657
1658 #[test]
1659 fn eval_negative_float() {
1660 let mut scope = Scope::new();
1661 let expr = Expr::Literal(Value::Float(-3.14));
1662 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1663 }
1664
1665 #[test]
1666 fn eval_zero_values() {
1667 let mut scope = Scope::new();
1668 assert_eq!(
1669 eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1670 Ok(Value::Int(0))
1671 );
1672 assert_eq!(
1673 eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1674 Ok(Value::Float(0.0))
1675 );
1676 }
1677
1678 #[test]
1679 fn eval_interpolation_empty_var() {
1680 let mut scope = Scope::new();
1681 scope.set("EMPTY", Value::String("".into()));
1682
1683 let expr = Expr::Interpolated(vec![
1684 StringPart::Literal("prefix".into()),
1685 StringPart::Var(VarPath::simple("EMPTY")),
1686 StringPart::Literal("suffix".into()),
1687 ]);
1688 assert_eq!(
1689 eval_expr(&expr, &mut scope),
1690 Ok(Value::String("prefixsuffix".into()))
1691 );
1692 }
1693
1694 #[test]
1695 fn eval_chained_and() {
1696 let mut scope = Scope::new();
1697 let expr = Expr::BinaryOp {
1699 left: Box::new(Expr::BinaryOp {
1700 left: Box::new(Expr::Literal(Value::Bool(true))),
1701 op: BinaryOp::And,
1702 right: Box::new(Expr::Literal(Value::Bool(true))),
1703 }),
1704 op: BinaryOp::And,
1705 right: Box::new(Expr::Literal(Value::Int(42))),
1706 };
1707 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1708 }
1709
1710 #[test]
1711 fn eval_chained_or() {
1712 let mut scope = Scope::new();
1713 let expr = Expr::BinaryOp {
1715 left: Box::new(Expr::BinaryOp {
1716 left: Box::new(Expr::Literal(Value::Bool(false))),
1717 op: BinaryOp::Or,
1718 right: Box::new(Expr::Literal(Value::Bool(false))),
1719 }),
1720 op: BinaryOp::Or,
1721 right: Box::new(Expr::Literal(Value::Int(42))),
1722 };
1723 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1724 }
1725
1726 #[test]
1727 fn eval_mixed_and_or() {
1728 let mut scope = Scope::new();
1729 let expr = Expr::BinaryOp {
1732 left: Box::new(Expr::BinaryOp {
1733 left: Box::new(Expr::Literal(Value::Bool(true))),
1734 op: BinaryOp::Or,
1735 right: Box::new(Expr::Literal(Value::Bool(false))),
1736 }),
1737 op: BinaryOp::And,
1738 right: Box::new(Expr::Literal(Value::Bool(true))),
1739 };
1740 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1742 }
1743
1744 #[test]
1745 fn eval_interpolation_with_bool() {
1746 let mut scope = Scope::new();
1747 scope.set("FLAG", Value::Bool(true));
1748
1749 let expr = Expr::Interpolated(vec![
1750 StringPart::Literal("enabled: ".into()),
1751 StringPart::Var(VarPath::simple("FLAG")),
1752 ]);
1753 assert_eq!(
1754 eval_expr(&expr, &mut scope),
1755 Ok(Value::String("enabled: true".into()))
1756 );
1757 }
1758
1759 #[test]
1760 fn eval_interpolation_with_null() {
1761 let mut scope = Scope::new();
1762 scope.set("VAL", Value::Null);
1763
1764 let expr = Expr::Interpolated(vec![
1765 StringPart::Literal("value: ".into()),
1766 StringPart::Var(VarPath::simple("VAL")),
1767 ]);
1768 assert_eq!(
1769 eval_expr(&expr, &mut scope),
1770 Ok(Value::String("value: null".into()))
1771 );
1772 }
1773
1774 #[test]
1775 fn eval_format_path_simple() {
1776 let path = VarPath::simple("X");
1777 assert_eq!(format_path(&path), "${X}");
1778 }
1779
1780 #[test]
1781 fn eval_format_path_nested() {
1782 let path = VarPath {
1783 segments: vec![
1784 VarSegment::Field("X".into()),
1785 VarSegment::Field("field".into()),
1786 ],
1787 };
1788 assert_eq!(format_path(&path), "${X.field}");
1789 }
1790
1791 #[test]
1792 fn type_name_all_types() {
1793 assert_eq!(type_name(&Value::Null), "null");
1794 assert_eq!(type_name(&Value::Bool(true)), "bool");
1795 assert_eq!(type_name(&Value::Int(1)), "int");
1796 assert_eq!(type_name(&Value::Float(1.0)), "float");
1797 assert_eq!(type_name(&Value::String("".into())), "string");
1798 }
1799
1800 #[test]
1801 fn expand_tilde_home() {
1802 let home = "/home/session";
1804 assert_eq!(expand_tilde("~", Some(home)), home);
1805 assert_eq!(expand_tilde("~/foo", Some(home)), format!("{}/foo", home));
1806 assert_eq!(
1807 expand_tilde("~/foo/bar", Some(home)),
1808 format!("{}/foo/bar", home)
1809 );
1810 }
1811
1812 #[test]
1813 fn expand_tilde_hermetic_no_home_does_not_leak_host() {
1814 assert_eq!(expand_tilde("~", None), "~");
1817 assert_eq!(expand_tilde("~/foo", None), "~/foo");
1818 }
1819
1820 #[test]
1821 fn expand_tilde_passthrough() {
1822 assert_eq!(expand_tilde("/home/user", Some("/h")), "/home/user");
1824 assert_eq!(expand_tilde("foo~bar", Some("/h")), "foo~bar");
1825 assert_eq!(expand_tilde("", Some("/h")), "");
1826 }
1827
1828 #[test]
1829 #[cfg(all(unix, feature = "host"))]
1830 fn expand_tilde_user() {
1831 let expanded = expand_tilde("~root", None);
1834 assert!(
1836 expanded == "/root" || expanded == "/var/root",
1837 "expected /root or /var/root, got: {}",
1838 expanded
1839 );
1840
1841 let expanded_path = expand_tilde("~root/subdir", None);
1843 assert!(
1844 expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1845 "expected /root/subdir or /var/root/subdir, got: {}",
1846 expanded_path
1847 );
1848
1849 let nonexistent = expand_tilde("~nonexistent_user_12345", None);
1851 assert_eq!(nonexistent, "~nonexistent_user_12345");
1852 }
1853
1854 #[test]
1855 fn value_to_string_with_tilde_expansion() {
1856 let val = Value::String("~/test".into());
1858 assert_eq!(
1859 value_to_string_with_tilde(&val, Some("/home/session")),
1860 "/home/session/test"
1861 );
1862 }
1863
1864 #[test]
1865 fn eval_positional_param() {
1866 let mut scope = Scope::new();
1867 scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1868
1869 let expr = Expr::Positional(0);
1871 let result = eval_expr(&expr, &mut scope).unwrap();
1872 assert_eq!(result, Value::String("my_tool".into()));
1873
1874 let expr = Expr::Positional(1);
1876 let result = eval_expr(&expr, &mut scope).unwrap();
1877 assert_eq!(result, Value::String("hello".into()));
1878
1879 let expr = Expr::Positional(2);
1881 let result = eval_expr(&expr, &mut scope).unwrap();
1882 assert_eq!(result, Value::String("world".into()));
1883
1884 let expr = Expr::Positional(3);
1886 let result = eval_expr(&expr, &mut scope).unwrap();
1887 assert_eq!(result, Value::String("".into()));
1888 }
1889
1890 #[test]
1891 fn eval_all_args() {
1892 let mut scope = Scope::new();
1893 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1894
1895 let expr = Expr::AllArgs;
1896 let result = eval_expr(&expr, &mut scope).unwrap();
1897
1898 assert_eq!(result, Value::String("a b c".into()));
1900 }
1901
1902 #[test]
1903 fn eval_arg_count() {
1904 let mut scope = Scope::new();
1905 scope.set_positional("test", vec!["x".into(), "y".into()]);
1906
1907 let expr = Expr::ArgCount;
1908 let result = eval_expr(&expr, &mut scope).unwrap();
1909 assert_eq!(result, Value::Int(2));
1910 }
1911
1912 #[test]
1913 fn eval_arg_count_empty() {
1914 let mut scope = Scope::new();
1915
1916 let expr = Expr::ArgCount;
1917 let result = eval_expr(&expr, &mut scope).unwrap();
1918 assert_eq!(result, Value::Int(0));
1919 }
1920
1921 #[test]
1922 fn eval_var_length_string() {
1923 let mut scope = Scope::new();
1924 scope.set("NAME", Value::String("hello".into()));
1925
1926 let expr = Expr::VarLength(VarPath::simple("NAME"));
1927 let result = eval_expr(&expr, &mut scope).unwrap();
1928 assert_eq!(result, Value::Int(5));
1929 }
1930
1931 #[test]
1932 fn eval_var_length_empty_string() {
1933 let mut scope = Scope::new();
1934 scope.set("EMPTY", Value::String("".into()));
1935
1936 let expr = Expr::VarLength(VarPath::simple("EMPTY"));
1937 let result = eval_expr(&expr, &mut scope).unwrap();
1938 assert_eq!(result, Value::Int(0));
1939 }
1940
1941 #[test]
1942 fn eval_var_length_unset() {
1943 let mut scope = Scope::new();
1944
1945 let expr = Expr::VarLength(VarPath::simple("MISSING"));
1947 let result = eval_expr(&expr, &mut scope).unwrap();
1948 assert_eq!(result, Value::Int(0));
1949 }
1950
1951 #[test]
1952 fn eval_var_length_int() {
1953 let mut scope = Scope::new();
1954 scope.set("NUM", Value::Int(12345));
1955
1956 let expr = Expr::VarLength(VarPath::simple("NUM"));
1958 let result = eval_expr(&expr, &mut scope).unwrap();
1959 assert_eq!(result, Value::Int(5)); }
1961
1962 #[test]
1963 fn eval_var_with_default_set() {
1964 let mut scope = Scope::new();
1965 scope.set("NAME", Value::String("Alice".into()));
1966
1967 let expr = Expr::VarWithDefault {
1969 path: VarPath::simple("NAME"),
1970 default: vec![StringPart::Literal("default".into())],
1971 };
1972 let result = eval_expr(&expr, &mut scope).unwrap();
1973 assert_eq!(result, Value::String("Alice".into()));
1974 }
1975
1976 #[test]
1977 fn eval_var_with_default_unset() {
1978 let mut scope = Scope::new();
1979
1980 let expr = Expr::VarWithDefault {
1982 path: VarPath::simple("MISSING"),
1983 default: vec![StringPart::Literal("fallback".into())],
1984 };
1985 let result = eval_expr(&expr, &mut scope).unwrap();
1986 assert_eq!(result, Value::String("fallback".into()));
1987 }
1988
1989 #[test]
1990 fn eval_var_with_default_empty() {
1991 let mut scope = Scope::new();
1992 scope.set("EMPTY", Value::String("".into()));
1993
1994 let expr = Expr::VarWithDefault {
1996 path: VarPath::simple("EMPTY"),
1997 default: vec![StringPart::Literal("not empty".into())],
1998 };
1999 let result = eval_expr(&expr, &mut scope).unwrap();
2000 assert_eq!(result, Value::String("not empty".into()));
2001 }
2002
2003 #[test]
2004 fn eval_var_with_default_non_string() {
2005 let mut scope = Scope::new();
2006 scope.set("NUM", Value::Int(42));
2007
2008 let expr = Expr::VarWithDefault {
2010 path: VarPath::simple("NUM"),
2011 default: vec![StringPart::Literal("default".into())],
2012 };
2013 let result = eval_expr(&expr, &mut scope).unwrap();
2014 assert_eq!(result, Value::Int(42));
2015 }
2016
2017 #[test]
2018 fn eval_unset_variable_is_empty() {
2019 let mut scope = Scope::new();
2020 let parts = vec![
2021 StringPart::Literal("prefix:".into()),
2022 StringPart::Var(VarPath::simple("UNSET")),
2023 StringPart::Literal(":suffix".into()),
2024 ];
2025 let expr = Expr::Interpolated(parts);
2026 let result = eval_expr(&expr, &mut scope).unwrap();
2027 assert_eq!(result, Value::String("prefix::suffix".into()));
2028 }
2029
2030 #[test]
2031 fn eval_unset_variable_multiple() {
2032 let mut scope = Scope::new();
2033 scope.set("SET", Value::String("hello".into()));
2034 let parts = vec![
2035 StringPart::Var(VarPath::simple("UNSET1")),
2036 StringPart::Literal("-".into()),
2037 StringPart::Var(VarPath::simple("SET")),
2038 StringPart::Literal("-".into()),
2039 StringPart::Var(VarPath::simple("UNSET2")),
2040 ];
2041 let expr = Expr::Interpolated(parts);
2042 let result = eval_expr(&expr, &mut scope).unwrap();
2043 assert_eq!(result, Value::String("-hello-".into()));
2044 }
2045
2046 #[test]
2049 fn values_equal_scalars_still_work() {
2050 assert_eq!(
2051 values_equal(&Value::String("x".into()), &Value::String("x".into())),
2052 Ok(true)
2053 );
2054 assert_eq!(
2056 values_equal(&Value::String("42".into()), &Value::Int(42)),
2057 Ok(true)
2058 );
2059 }
2060
2061 #[test]
2062 fn values_equal_collection_vs_scalar_is_loud() {
2063 let list = Value::Json(serde_json::json!(["a", "b"]));
2064 let record = Value::Json(serde_json::json!({"k": 1}));
2065 assert!(
2066 matches!(values_equal(&list, &Value::String("banana".into())), Err(EvalError::Unsupported(_))),
2067 "list vs scalar must be a loud error, never silently false"
2068 );
2069 assert!(matches!(
2071 values_equal(&Value::String("x".into()), &record),
2072 Err(EvalError::Unsupported(_))
2073 ));
2074 }
2075
2076 #[test]
2077 fn values_equal_collection_vs_collection_is_structural() {
2078 let a = Value::Json(serde_json::json!({"a": 1, "b": 2}));
2080 let b = Value::Json(serde_json::json!({"b": 2, "a": 1}));
2081 assert_eq!(values_equal(&a, &b), Ok(true));
2082 }
2083
2084 #[test]
2087 fn values_equal_bytes_vs_bytes_still_works() {
2088 assert_eq!(
2090 values_equal(&Value::Bytes(vec![1, 2, 3]), &Value::Bytes(vec![1, 2, 3])),
2091 Ok(true)
2092 );
2093 assert_eq!(
2094 values_equal(&Value::Bytes(vec![1, 2, 3]), &Value::Bytes(vec![1, 2, 4])),
2095 Ok(false)
2096 );
2097 }
2098
2099 #[test]
2100 fn values_equal_bytes_vs_scalar_is_loud() {
2101 let bin = Value::Bytes(vec![0xff, 0x00]);
2105 assert!(matches!(
2106 values_equal(&bin, &Value::String("x".into())),
2107 Err(EvalError::Unsupported(_))
2108 ));
2109 assert!(matches!(
2110 values_equal(&Value::Int(1), &bin),
2111 Err(EvalError::Unsupported(_))
2112 ));
2113 }
2114
2115 #[test]
2116 fn eval_membership_bytes_needle_against_record_key_is_loud() {
2117 let record = Value::Json(serde_json::json!({"k": 1}));
2118 let bin = Value::Bytes(vec![0xff, 0x00]);
2119 assert!(matches!(
2120 eval_membership(&bin, &record),
2121 Err(EvalError::Unsupported(_))
2122 ));
2123 }
2124
2125 #[test]
2126 fn eval_membership_bytes_needle_against_list_is_not_a_match_not_an_abort() {
2127 let list = Value::Json(serde_json::json!(["a", "b"]));
2131 let bin = Value::Bytes(vec![0xff, 0x00]);
2132 assert_eq!(eval_membership(&bin, &list), Ok(false));
2133 }
2134
2135 #[test]
2136 fn value_length_of_bytes_is_byte_count() {
2137 assert_eq!(value_length(&Value::Bytes(vec![1, 2, 3])), 3);
2138 }
2139
2140 #[test]
2141 fn structured_export_error_flags_collections_passes_scalars() {
2142 let scalars = vec![
2144 ("A".to_string(), Value::String("x".into())),
2145 ("B".to_string(), Value::Int(1)),
2146 ];
2147 assert!(structured_export_error(&scalars).is_none());
2148 let with_record = vec![(
2150 "CFG".to_string(),
2151 Value::Json(serde_json::json!({"port": 8080})),
2152 )];
2153 let msg = structured_export_error(&with_record).expect("record must be refused");
2154 assert!(msg.contains("CFG") && msg.contains("tojson"), "got: {msg}");
2155 let with_list = vec![("XS".to_string(), Value::Json(serde_json::json!([1, 2])))];
2157 assert!(structured_export_error(&with_list).is_some());
2158 }
2159
2160 #[test]
2161 fn defaults_on_emptiness_matches_decision_a() {
2162 assert!(value_defaults_on_emptiness(&Value::Null));
2165 assert!(value_defaults_on_emptiness(&Value::Json(serde_json::Value::Null)));
2166 assert!(value_defaults_on_emptiness(&Value::String(String::new())));
2167 assert!(!value_defaults_on_emptiness(&Value::Bool(false)));
2168 assert!(!value_defaults_on_emptiness(&Value::Int(0)));
2169 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!([]))));
2170 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!({}))));
2171 assert!(!value_defaults_on_emptiness(&Value::String("x".into())));
2172 }
2173
2174 #[test]
2175 fn subscripted_length_and_default_resolve_the_path() {
2176 let mut scope = Scope::new();
2179 scope.set("u", Value::Json(serde_json::json!({"tags": ["a", "b"]})));
2180 let len = eval_expr(
2181 &Expr::VarLength(crate::parser::parse_varpath("${u[tags]}")),
2182 &mut scope,
2183 )
2184 .unwrap();
2185 assert_eq!(len, Value::Int(2));
2186
2187 scope.set("cfg", Value::Json(serde_json::json!({"port": 9000})));
2188 let val = eval_expr(
2190 &Expr::VarWithDefault {
2191 path: crate::parser::parse_varpath("${cfg[port]}"),
2192 default: vec![StringPart::Literal("8080".into())],
2193 },
2194 &mut scope,
2195 )
2196 .unwrap();
2197 assert_eq!(value_to_string(&val), "9000");
2198
2199 let missing = eval_expr(
2201 &Expr::VarWithDefault {
2202 path: crate::parser::parse_varpath("${cfg[nope]}"),
2203 default: vec![StringPart::Literal("8080".into())],
2204 },
2205 &mut scope,
2206 )
2207 .unwrap();
2208 assert_eq!(value_to_string(&missing), "8080");
2209
2210 let err = eval_expr(
2212 &Expr::VarWithDefault {
2213 path: crate::parser::parse_varpath("${cfg[0]}"),
2214 default: vec![StringPart::Literal("x".into())],
2215 },
2216 &mut scope,
2217 )
2218 .unwrap_err();
2219 assert!(matches!(err, EvalError::InvalidPath(_)), "got: {err}");
2220 }
2221}