1use std::borrow::Cow;
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11
12use kaish_types::{json_to_value_no_envelope, value_to_json};
13
14use crate::ast::{Value, VarPath, VarSegment};
15
16use super::eval::value_to_string;
17use super::result::ExecResult;
18
19#[derive(Debug, Clone, PartialEq)]
39#[non_exhaustive]
40pub enum PathError {
41 UndefinedRoot(String),
43 Absence(String),
45 Shape(String),
47}
48
49fn type_name(value: &Value) -> &'static str {
51 match value {
52 Value::Null => "null",
53 Value::Bool(_) => "a boolean",
54 Value::Int(_) => "an integer",
55 Value::Float(_) => "a float",
56 Value::String(_) => "a string",
57 Value::Json(serde_json::Value::Array(_)) => "a list",
58 Value::Json(serde_json::Value::Object(_)) => "a record",
59 Value::Json(_) => "a scalar",
60 Value::Bytes(_) => "binary data",
61 }
62}
63
64fn value_as_index(value: &Value) -> Option<i64> {
67 match value {
68 Value::Int(i) => Some(*i),
69 Value::String(s) => s.parse::<i64>().ok(),
70 _ => None,
71 }
72}
73
74#[derive(Debug, Clone, PartialEq)]
81enum Step {
82 Index(usize),
84 Key(String),
86 Slice(usize, usize),
88}
89
90fn classify_index(json: &serde_json::Value, i: i64, path: &str) -> Result<Step, PathError> {
96 let arr = match json {
97 serde_json::Value::Array(a) => a,
98 serde_json::Value::Object(_) => {
99 return Err(PathError::Shape(format!(
100 "${{{path}[{i}]}}: integer index on a record — record keys are strings, use ${{{path}[\"{i}\"]}}"
101 )))
102 }
103 _ => unreachable!("resolve_step guards non-collection containers"),
104 };
105 let len = arr.len() as i64;
106 let idx = if i < 0 { len + i } else { i };
107 if idx < 0 || idx >= len {
108 return Err(PathError::Absence(format!(
109 "${{{path}[{i}]}}: index out of bounds (list length {len})"
110 )));
111 }
112 Ok(Step::Index(idx as usize))
113}
114
115fn without_leading_zeros(key: &str) -> Option<String> {
121 let mut changed = false;
122 let fixed = key
123 .split(':')
124 .map(|part| {
125 if !crate::lexer::is_leading_zero_numeral(part) {
126 return part.to_string();
127 }
128 changed = true;
129 let sign = if part.starts_with('-') { "-" } else { "" };
130 let digits = part.trim_start_matches('-').trim_start_matches('0');
131 format!("{sign}{}", if digits.is_empty() { "0" } else { digits })
132 })
133 .collect::<Vec<_>>()
134 .join(":");
135 changed.then_some(fixed)
136}
137
138fn classify_key(json: &serde_json::Value, key: &str, path: &str) -> Result<Step, PathError> {
142 match json {
143 serde_json::Value::Object(_) => Ok(Step::Key(key.to_string())),
144 serde_json::Value::Array(_) if let Some(fix) = without_leading_zeros(key) => {
145 Err(PathError::Shape(format!(
148 "${{{path}[{key}]}}: `{key}` is text (leading zero) and a list is indexed by \
149 number — write ${{{path}[{fix}]}}"
150 )))
151 }
152 serde_json::Value::Array(_) => Err(PathError::Shape(format!(
153 "${{{path}[{key}]}}: string key on a list — use an integer index"
154 ))),
155 _ => unreachable!("resolve_step guards non-collection containers"),
156 }
157}
158
159fn classify_slice(
163 json: &serde_json::Value,
164 start: Option<i64>,
165 end: Option<i64>,
166 path: &str,
167) -> Result<Step, PathError> {
168 let len = match json {
171 serde_json::Value::Array(a) => a.len() as i64,
172 serde_json::Value::String(s) => s.chars().count() as i64,
173 serde_json::Value::Object(_) => {
174 return Err(PathError::Shape(format!(
175 "${{{path}[..]}}: cannot slice a record"
176 )))
177 }
178 _ => unreachable!("resolve_step guards non-sliceable containers"),
179 };
180 let norm = |b: i64| -> i64 {
181 let b = if b < 0 { len + b } else { b };
182 b.clamp(0, len)
183 };
184 let s = start.map(norm).unwrap_or(0);
185 let e = end.map(norm).unwrap_or(len);
186 let (s, e) = if s >= e {
187 (s as usize, s as usize)
188 } else {
189 (s as usize, e as usize)
190 };
191 Ok(Step::Slice(s, e))
192}
193
194fn dotted_access_error(path: &str, field: &str) -> PathError {
198 PathError::Shape(format!(
199 "${{{path}…}}: kaish uses bracket access, not dots — write the key as a subscript: [{field}]"
200 ))
201}
202
203fn render_segment(seg: &VarSegment) -> String {
207 match seg {
208 VarSegment::Index(i) => format!("[{i}]"),
209 VarSegment::Key(k) => format!("[{k}]"),
210 VarSegment::Dynamic(v) => format!("[${v}]"),
211 VarSegment::Slice(a, b) => format!(
212 "[{}:{}]",
213 a.map(|n| n.to_string()).unwrap_or_default(),
214 b.map(|n| n.to_string()).unwrap_or_default()
215 ),
216 VarSegment::Field(f) => format!(".{f}"),
217 }
218}
219
220fn resolve_step(
226 container: &serde_json::Value,
227 seg: &VarSegment,
228 scope: &Scope,
229 path: &str,
230) -> Result<Step, PathError> {
231 if let VarSegment::Field(name) = seg {
235 return Err(dotted_access_error(path, name));
236 }
237
238 if matches!(container, serde_json::Value::String(_)) {
243 return match seg {
244 VarSegment::Slice(start, end) => classify_slice(container, *start, *end, path),
245 _ => Err(PathError::Shape(format!(
246 "${{{path}…}}: cannot subscript a string — slice it instead, \
247 e.g. ${{{path}[0:5]}} for the first five characters"
248 ))),
249 };
250 }
251
252 if !matches!(
254 container,
255 serde_json::Value::Array(_) | serde_json::Value::Object(_)
256 ) {
257 return Err(PathError::Shape(format!(
258 "${{{path}…}}: cannot subscript {} — it is not a collection",
259 type_name(&json_to_value_no_envelope(container.clone()))
260 )));
261 }
262
263 match seg {
264 VarSegment::Index(i) => classify_index(container, *i, path),
265 VarSegment::Key(k) => classify_key(container, k, path),
266 VarSegment::Slice(start, end) => classify_slice(container, *start, *end, path),
267 VarSegment::Dynamic(var) => {
268 let key_val = scope.get(var).ok_or_else(|| {
272 PathError::UndefinedRoot(format!("${{{path}[${var}]}}: ${var} is not set"))
273 })?;
274 match container {
275 serde_json::Value::Array(_) => {
276 let idx = value_as_index(key_val).ok_or_else(|| {
277 PathError::Shape(format!(
278 "${{{path}[${var}]}}: a list index must be an integer, got \"{}\"",
279 value_to_string(key_val)
280 ))
281 })?;
282 classify_index(container, idx, path)
283 }
284 serde_json::Value::Object(_) => Ok(Step::Key(value_to_string(key_val))),
285 _ => unreachable!("non-collection container guarded above"),
286 }
287 }
288 VarSegment::Field(_) => unreachable!("dotted segment handled above"),
289 }
290}
291
292fn descend<'a>(
298 current: Cow<'a, serde_json::Value>,
299 step: Step,
300 path: &str,
301) -> Result<Cow<'a, serde_json::Value>, PathError> {
302 match step {
303 Step::Slice(s, e) => match current.as_ref() {
304 serde_json::Value::Array(arr) => {
305 Ok(Cow::Owned(serde_json::Value::Array(arr[s..e].to_vec())))
306 }
307 serde_json::Value::String(text) => Ok(Cow::Owned(serde_json::Value::String(
309 text.chars().skip(s).take(e - s).collect(),
310 ))),
311 _ => unreachable!("slice classified against an array or string"),
312 },
313 Step::Index(i) => match current {
314 Cow::Borrowed(j) => {
315 let Some(arr) = j.as_array() else {
316 unreachable!("index classified against an array")
317 };
318 Ok(Cow::Borrowed(&arr[i]))
319 }
320 Cow::Owned(j) => {
321 let Some(arr) = j.as_array() else {
322 unreachable!("index classified against an array")
323 };
324 Ok(Cow::Owned(arr[i].clone()))
325 }
326 },
327 Step::Key(k) => match current {
328 Cow::Borrowed(j) => match j.as_object().and_then(|m| m.get(&k)) {
329 Some(child) => Ok(Cow::Borrowed(child)),
330 None => Err(PathError::Absence(format!("${{{path}[{k}]}}: no such key"))),
331 },
332 Cow::Owned(j) => match j.as_object().and_then(|m| m.get(&k)) {
333 Some(child) => Ok(Cow::Owned(child.clone())),
334 None => Err(PathError::Absence(format!("${{{path}[{k}]}}: no such key"))),
335 },
336 },
337 }
338}
339
340fn descend_mut<'a>(
350 current: &'a mut serde_json::Value,
351 step: Step,
352 path: &str,
353) -> Result<&'a mut serde_json::Value, PathError> {
354 match step {
355 Step::Slice(..) => Err(PathError::Shape(format!(
356 "${{{path}[..]}}: slice lvalues are not supported — index or key paths only"
357 ))),
358 Step::Index(i) => {
359 let Some(arr) = current.as_array_mut() else {
360 unreachable!("index classified against an array")
361 };
362 Ok(&mut arr[i])
363 }
364 Step::Key(k) => {
365 let Some(map) = current.as_object_mut() else {
366 unreachable!("key classified against an object")
367 };
368 match map.get_mut(&k) {
369 Some(child) => Ok(child),
370 None => Err(PathError::Absence(format!(
371 "${{{path}[{k}]}}: no such key — no autovivification, create it first (e.g. `{path}[{k}]={{}}`)"
372 ))),
373 }
374 }
375 }
376}
377
378fn apply_leaf_write(
385 current: &mut serde_json::Value,
386 step: Step,
387 value: serde_json::Value,
388 path: &str,
389) -> Result<(), PathError> {
390 match step {
391 Step::Slice(..) => Err(PathError::Shape(format!(
392 "${{{path}[..]}}: slice lvalues are not supported — index or key paths only"
393 ))),
394 Step::Index(i) => {
395 let Some(arr) = current.as_array_mut() else {
396 unreachable!("index classified against an array")
397 };
398 arr[i] = value;
399 Ok(())
400 }
401 Step::Key(k) => {
402 let Some(map) = current.as_object_mut() else {
403 unreachable!("key classified against an object")
404 };
405 map.insert(k, value);
406 Ok(())
407 }
408 }
409}
410
411fn push_path_error_message(err: PathError, root_name: &str) -> String {
418 match err {
419 PathError::UndefinedRoot(msg) if msg.is_empty() => {
420 format!("push: {root_name} is not defined")
421 }
422 PathError::UndefinedRoot(msg) => format!("push: {msg}"),
423 PathError::Absence(msg) | PathError::Shape(msg) => msg,
424 }
425}
426
427#[derive(Debug, Clone)]
438pub struct Scope {
439 frames: Arc<Vec<HashMap<String, Value>>>,
442 exported: HashSet<String>,
444 last_result: Box<ExecResult>,
452 last_cmdsubst_code: Option<i64>,
459 script_name: String,
461 positional: Vec<String>,
463 error_exit: bool,
465 errexit_suppressed: usize,
468 show_ast: bool,
470 trash_enabled: bool,
472 trash_max_size: u64,
475 glob_enabled: bool,
477 pipefail_enabled: bool,
480 pid: u64,
486}
487
488impl Scope {
489 pub fn new() -> Self {
494 Self {
495 frames: Arc::new(vec![HashMap::new()]),
496 exported: HashSet::new(),
497 last_result: Box::new(ExecResult::default()),
498 last_cmdsubst_code: None,
499 script_name: String::new(),
500 positional: Vec::new(),
501 error_exit: false,
502 errexit_suppressed: 0,
503 show_ast: false,
504 trash_enabled: false,
505 trash_max_size: 10 * 1024 * 1024, glob_enabled: true,
507 pipefail_enabled: false,
508 pid: 0,
509 }
510 }
511
512 pub fn pid(&self) -> u64 {
514 self.pid
515 }
516
517 pub fn set_pid(&mut self, pid: u64) {
521 self.pid = pid;
522 }
523
524 pub fn push_frame(&mut self) {
526 Arc::make_mut(&mut self.frames).push(HashMap::new());
527 }
528
529 pub fn pop_frame(&mut self) {
533 if self.frames.len() > 1 {
534 Arc::make_mut(&mut self.frames).pop();
535 } else {
536 panic!("cannot pop the root scope frame");
537 }
538 }
539
540 pub fn set(&mut self, name: impl Into<String>, value: Value) {
550 let name = crate::ast::normalize_name(name.into());
551 if let Some(frame) = Arc::make_mut(&mut self.frames).last_mut() {
552 frame.insert(name, value);
553 }
554 }
555
556 pub fn set_global(&mut self, name: impl Into<String>, value: Value) {
562 let name = crate::ast::normalize_name(name.into());
563
564 let frames = Arc::make_mut(&mut self.frames);
566 for frame in frames.iter_mut().rev() {
567 if let std::collections::hash_map::Entry::Occupied(mut e) = frame.entry(name.clone()) {
568 e.insert(value);
569 return;
570 }
571 }
572
573 if let Some(frame) = frames.first_mut() {
575 frame.insert(name, value);
576 }
577 }
578
579 pub fn get(&self, name: &str) -> Option<&Value> {
581 let normalized;
582 let name = if name.is_ascii() {
583 name
584 } else {
585 normalized = crate::ast::normalize_name(name.to_string());
586 normalized.as_str()
587 };
588 for frame in self.frames.iter().rev() {
589 if let Some(value) = frame.get(name) {
590 return Some(value);
591 }
592 }
593 None
594 }
595
596 pub fn remove(&mut self, name: &str) -> Option<Value> {
600 let normalized;
601 let name = if name.is_ascii() {
602 name
603 } else {
604 normalized = crate::ast::normalize_name(name.to_string());
605 normalized.as_str()
606 };
607 for frame in Arc::make_mut(&mut self.frames).iter_mut().rev() {
608 if let Some(value) = frame.remove(name) {
609 return Some(value);
610 }
611 }
612 None
613 }
614
615 pub fn set_last_result(&mut self, result: ExecResult) {
617 *self.last_result = result;
619 }
620
621 pub fn last_result(&self) -> &ExecResult {
623 &self.last_result
624 }
625
626 pub fn note_cmdsubst_code(&mut self, code: i64) {
630 self.last_cmdsubst_code = Some(code);
631 }
632
633 pub fn clear_cmdsubst_code(&mut self) {
637 self.last_cmdsubst_code = None;
638 }
639
640 pub fn take_cmdsubst_code(&mut self) -> Option<i64> {
642 self.last_cmdsubst_code.take()
643 }
644
645 pub fn set_positional(&mut self, script_name: impl Into<String>, args: Vec<String>) {
649 self.script_name = script_name.into();
650 self.positional = args;
651 }
652
653 pub fn save_positional(&self) -> (String, Vec<String>) {
657 (self.script_name.clone(), self.positional.clone())
658 }
659
660 pub fn get_positional(&self, n: usize) -> Option<&str> {
664 if n == 0 {
665 if self.script_name.is_empty() {
666 None
667 } else {
668 Some(&self.script_name)
669 }
670 } else {
671 self.positional.get(n - 1).map(|s| s.as_str())
672 }
673 }
674
675 pub fn all_args(&self) -> &[String] {
677 &self.positional
678 }
679
680 pub fn arg_count(&self) -> usize {
682 self.positional.len()
683 }
684
685 pub fn error_exit_enabled(&self) -> bool {
690 self.error_exit && self.errexit_suppressed == 0
691 }
692
693 pub fn error_exit_flag(&self) -> bool {
701 self.error_exit
702 }
703
704 pub fn set_error_exit(&mut self, enabled: bool) {
706 self.error_exit = enabled;
707 }
708
709 pub fn suppress_errexit(&mut self) {
711 self.errexit_suppressed += 1;
712 }
713
714 pub fn unsuppress_errexit(&mut self) {
716 self.errexit_suppressed = self.errexit_suppressed.saturating_sub(1);
717 }
718
719 pub fn show_ast(&self) -> bool {
721 self.show_ast
722 }
723
724 pub fn set_show_ast(&mut self, enabled: bool) {
726 self.show_ast = enabled;
727 }
728
729 pub fn pipefail_enabled(&self) -> bool {
731 self.pipefail_enabled
732 }
733
734 pub fn set_pipefail_enabled(&mut self, enabled: bool) {
736 self.pipefail_enabled = enabled;
737 }
738
739 pub fn set_pipestatus(&mut self, codes: &[i64]) {
747 let list = serde_json::Value::Array(
748 codes.iter().map(|c| serde_json::Value::from(*c)).collect(),
749 );
750 self.set_global("PIPESTATUS", Value::Json(list));
751 }
752
753 pub fn pipestatus_rightmost_failure(&self) -> Option<i64> {
761 let Some(Value::Json(serde_json::Value::Array(codes))) = self.get("PIPESTATUS") else {
762 return None;
763 };
764 codes
765 .iter()
766 .filter_map(serde_json::Value::as_i64)
767 .rfind(|c| *c != 0)
768 }
769
770 pub fn trash_enabled(&self) -> bool {
772 self.trash_enabled
773 }
774
775 pub fn set_trash_enabled(&mut self, enabled: bool) {
777 self.trash_enabled = enabled;
778 }
779
780 pub fn trash_max_size(&self) -> u64 {
782 self.trash_max_size
783 }
784
785 pub fn set_trash_max_size(&mut self, size: u64) {
787 self.trash_max_size = size;
788 }
789
790 pub fn glob_enabled(&self) -> bool {
792 self.glob_enabled
793 }
794
795 pub fn set_glob_enabled(&mut self, enabled: bool) {
797 self.glob_enabled = enabled;
798 }
799
800 pub fn export(&mut self, name: impl Into<String>) {
804 self.exported.insert(name.into());
805 }
806
807 pub fn is_exported(&self, name: &str) -> bool {
809 self.exported.contains(name)
810 }
811
812 pub fn set_exported(&mut self, name: impl Into<String>, value: Value) {
819 let name = name.into();
820 self.set(&name, value);
821 self.export(name);
822 }
823
824 pub fn set_exported_global(&mut self, name: impl Into<String>, value: Value) {
830 let name = name.into();
831 self.set_global(&name, value);
832 self.export(name);
833 }
834
835 pub fn unexport(&mut self, name: &str) {
837 self.exported.remove(name);
838 }
839
840 pub fn exported_vars(&self) -> Vec<(String, Value)> {
844 let mut result = Vec::new();
845 for name in &self.exported {
846 if let Some(value) = self.get(name) {
847 result.push((name.clone(), value.clone()));
848 }
849 }
850 result.sort_by(|(a, _), (b, _)| a.cmp(b));
851 result
852 }
853
854 pub fn exported_names(&self) -> Vec<&str> {
856 let mut names: Vec<&str> = self.exported.iter().map(|s| s.as_str()).collect();
857 names.sort();
858 names
859 }
860
861 pub fn resolve_path(&self, path: &VarPath) -> Result<Value, PathError> {
878 let Some(VarSegment::Field(root_name)) = path.segments.first() else {
879 return Err(PathError::UndefinedRoot(String::new()));
881 };
882
883 if root_name == "?" {
885 if path.segments.len() == 1 {
886 return Ok(Value::Int(self.last_result.code));
887 }
888 return Err(PathError::Shape(
889 "$? is the POSIX exit code, not a collection — use `kaish-last` for structured data"
890 .to_string(),
891 ));
892 }
893
894 let root = self
895 .get(root_name)
896 .ok_or_else(|| PathError::UndefinedRoot(root_name.clone()))?;
897
898 let subscripts = &path.segments[1..];
901 if subscripts.is_empty() {
902 return Ok(root.clone());
903 }
904
905 if let Some(VarSegment::Field(name)) = subscripts.first() {
910 return Err(dotted_access_error(root_name, name));
911 }
912
913 let lifted;
920 let root_json = match root {
921 Value::Json(j) => j,
922 Value::String(s) => {
923 lifted = serde_json::Value::String(s.clone());
924 &lifted
925 }
926 other => {
927 return Err(PathError::Shape(format!(
928 "${{{root_name}…}}: cannot subscript {} — it is not a collection",
929 type_name(other)
930 )))
931 }
932 };
933
934 let mut current = Cow::Borrowed(root_json);
939 let mut prefix = root_name.clone();
940 for seg in subscripts {
941 let step = resolve_step(¤t, seg, self, &prefix)?;
942 current = descend(current, step, &prefix)?;
943 prefix.push_str(&render_segment(seg));
944 }
945 Ok(json_to_value_no_envelope(current.into_owned()))
946 }
947
948 pub fn walk_write(&mut self, path: &VarPath, value: Value) -> Result<(), PathError> {
968 let Some(VarSegment::Field(root_name)) = path.segments.first() else {
969 return Err(PathError::UndefinedRoot(String::new()));
970 };
971
972 let root = self
973 .get(root_name)
974 .ok_or_else(|| PathError::UndefinedRoot(root_name.clone()))?;
975
976 let mut root_json = match root {
977 Value::Json(j) => j.clone(),
978 other => {
979 return Err(PathError::Shape(format!(
980 "${{{root_name}…}}: cannot subscript {} — it is not a collection",
981 type_name(other)
982 )))
983 }
984 };
985
986 let subscripts = &path.segments[1..];
987 let Some((last, intermediates)) = subscripts.split_last() else {
988 return Err(PathError::Shape(format!(
992 "{root_name}: assignment target has no subscript"
993 )));
994 };
995
996 let mut current = &mut root_json;
997 let mut prefix = root_name.clone();
998 for seg in intermediates {
999 let step = resolve_step(current, seg, self, &prefix)?;
1000 current = descend_mut(current, step, &prefix)?;
1001 prefix.push_str(&render_segment(seg));
1002 }
1003
1004 let step = resolve_step(current, last, self, &prefix)?;
1005 apply_leaf_write(current, step, value_to_json(&value), &prefix)?;
1006
1007 self.set_global(root_name.clone(), Value::Json(root_json));
1008 Ok(())
1009 }
1010
1011 pub fn walk_append(&mut self, path: &VarPath, values: Vec<Value>) -> Result<(), String> {
1023 let Some(VarSegment::Field(root_name)) = path.segments.first() else {
1024 return Err("push: target has no root".to_string());
1025 };
1026 let root_name = root_name.clone();
1027 let current = self
1028 .get(&root_name)
1029 .ok_or_else(|| format!("push: {root_name} is not defined"))?
1030 .clone();
1031
1032 let subscripts = &path.segments[1..];
1033 if subscripts.is_empty() {
1034 if !matches!(current, Value::Json(serde_json::Value::Array(_))) {
1035 return Err(format!("push: {root_name} is not a list ({})", type_name(¤t)));
1036 }
1037 let Value::Json(serde_json::Value::Array(mut arr)) = current else {
1038 unreachable!("checked above")
1039 };
1040 arr.extend(values.iter().map(value_to_json));
1041 self.set_global(root_name, Value::Json(serde_json::Value::Array(arr)));
1042 return Ok(());
1043 }
1044
1045 let mut root_json = match current {
1046 Value::Json(j) => j,
1047 other => {
1048 return Err(format!(
1049 "push: {root_name}…: cannot subscript {} — it is not a collection",
1050 type_name(&other)
1051 ))
1052 }
1053 };
1054
1055 let mut cur = &mut root_json;
1059 let mut prefix = root_name.clone();
1060 for seg in subscripts {
1061 let step = resolve_step(cur, seg, self, &prefix)
1062 .map_err(|e| push_path_error_message(e, &root_name))?;
1063 cur = descend_mut(cur, step, &prefix)
1064 .map_err(|e| push_path_error_message(e, &root_name))?;
1065 prefix.push_str(&render_segment(seg));
1066 }
1067
1068 let serde_json::Value::Array(arr) = cur else {
1069 return Err(format!(
1070 "push: {prefix} is not a list ({})",
1071 type_name(&json_to_value_no_envelope(cur.clone()))
1072 ));
1073 };
1074 arr.extend(values.iter().map(value_to_json));
1075 self.set_global(root_name, Value::Json(root_json));
1076 Ok(())
1077 }
1078
1079 pub fn contains(&self, name: &str) -> bool {
1081 self.get(name).is_some()
1082 }
1083
1084 pub fn all_names(&self) -> Vec<&str> {
1086 let mut names: Vec<&str> = self
1087 .frames
1088 .iter()
1089 .flat_map(|f| f.keys().map(|s| s.as_str()))
1090 .collect();
1091 names.sort();
1092 names.dedup();
1093 names
1094 }
1095
1096 pub fn all(&self) -> Vec<(String, Value)> {
1100 let mut result = std::collections::HashMap::new();
1101 for frame in self.frames.iter() {
1103 for (name, value) in frame {
1104 result.insert(name.clone(), value.clone());
1105 }
1106 }
1107 let mut pairs: Vec<_> = result.into_iter().collect();
1108 pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
1109 pairs
1110 }
1111}
1112
1113impl Default for Scope {
1114 fn default() -> Self {
1115 Self::new()
1116 }
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121 use super::*;
1122
1123 #[test]
1124 fn new_scope_has_one_frame() {
1125 let scope = Scope::new();
1126 assert_eq!(scope.frames.len(), 1);
1127 }
1128
1129 #[test]
1130 fn set_and_get_variable() {
1131 let mut scope = Scope::new();
1132 scope.set("X", Value::Int(42));
1133 assert_eq!(scope.get("X"), Some(&Value::Int(42)));
1134 }
1135
1136 #[test]
1137 fn get_nonexistent_returns_none() {
1138 let scope = Scope::new();
1139 assert_eq!(scope.get("MISSING"), None);
1140 }
1141
1142 #[test]
1143 fn inner_frame_shadows_outer() {
1144 let mut scope = Scope::new();
1145 scope.set("X", Value::Int(1));
1146 scope.push_frame();
1147 scope.set("X", Value::Int(2));
1148 assert_eq!(scope.get("X"), Some(&Value::Int(2)));
1149 scope.pop_frame();
1150 assert_eq!(scope.get("X"), Some(&Value::Int(1)));
1151 }
1152
1153 #[test]
1154 fn inner_frame_can_see_outer_vars() {
1155 let mut scope = Scope::new();
1156 scope.set("OUTER", Value::String("visible".into()));
1157 scope.push_frame();
1158 assert_eq!(scope.get("OUTER"), Some(&Value::String("visible".into())));
1159 }
1160
1161 #[test]
1162 fn resolve_simple_path() {
1163 let mut scope = Scope::new();
1164 scope.set("NAME", Value::String("Alice".into()));
1165
1166 let path = VarPath::simple("NAME");
1167 assert_eq!(
1168 scope.resolve_path(&path),
1169 Ok(Value::String("Alice".into()))
1170 );
1171 }
1172
1173 #[test]
1174 fn resolve_bare_last_result_returns_exit_code() {
1175 let mut scope = Scope::new();
1176 scope.set_last_result(ExecResult::failure(127, "not found"));
1177
1178 let path = VarPath {
1179 segments: vec![VarSegment::Field("?".into())],
1180 };
1181 assert_eq!(scope.resolve_path(&path), Ok(Value::Int(127)));
1182 }
1183
1184 #[test]
1185 fn resolve_last_result_field_access_is_rejected() {
1186 let mut scope = Scope::new();
1190 scope.set_last_result(ExecResult::success_with_data(
1191 "1",
1192 Value::Json(serde_json::json!({"count": 5})),
1193 ));
1194
1195 let path = VarPath {
1196 segments: vec![
1197 VarSegment::Field("?".into()),
1198 VarSegment::Field("data".into()),
1199 ],
1200 };
1201 assert!(matches!(
1202 scope.resolve_path(&path),
1203 Err(PathError::Shape(_))
1204 ));
1205 }
1206
1207 #[test]
1208 fn resolve_dotted_access_on_scalar_is_a_loud_error() {
1209 let mut scope = Scope::new();
1210 scope.set("X", Value::Int(42));
1211
1212 let path = VarPath {
1214 segments: vec![
1215 VarSegment::Field("X".into()),
1216 VarSegment::Field("invalid".into()),
1217 ],
1218 };
1219 assert!(matches!(
1220 scope.resolve_path(&path),
1221 Err(PathError::Shape(_))
1222 ));
1223 }
1224
1225 #[test]
1226 fn resolve_undefined_root_is_soft() {
1227 let scope = Scope::new();
1228 let path = VarPath::simple("NOPE");
1229 assert!(matches!(
1230 scope.resolve_path(&path),
1231 Err(PathError::UndefinedRoot(_))
1232 ));
1233 }
1234
1235 fn subscripted(scope: &mut Scope, root: &str, value: serde_json::Value, seg: VarSegment) -> Result<Value, PathError> {
1242 scope.set(root, Value::Json(value));
1243 let path = VarPath {
1244 segments: vec![VarSegment::Field(root.into()), seg],
1245 };
1246 scope.resolve_path(&path)
1247 }
1248
1249 #[test]
1250 fn out_of_bounds_index_is_absence() {
1251 let mut scope = Scope::new();
1252 let r = subscripted(&mut scope, "xs", serde_json::json!([1, 2]), VarSegment::Index(9));
1253 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1254 }
1255
1256 #[test]
1257 fn missing_record_key_is_absence() {
1258 let mut scope = Scope::new();
1259 let r = subscripted(&mut scope, "u", serde_json::json!({"name": "amy"}), VarSegment::Key("nope".into()));
1260 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1261 }
1262
1263 #[test]
1264 fn string_key_on_a_list_is_shape() {
1265 let mut scope = Scope::new();
1266 let r = subscripted(&mut scope, "xs", serde_json::json!([1, 2]), VarSegment::Key("web".into()));
1267 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1268 }
1269
1270 #[test]
1271 fn integer_index_on_a_record_is_shape() {
1272 let mut scope = Scope::new();
1273 let r = subscripted(&mut scope, "u", serde_json::json!({"name": "amy"}), VarSegment::Index(0));
1274 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1275 }
1276
1277 #[test]
1278 fn subscripting_a_scalar_is_shape() {
1279 let mut scope = Scope::new();
1280 scope.set("s", Value::String("hello".into()));
1281 let path = VarPath {
1282 segments: vec![VarSegment::Field("s".into()), VarSegment::Index(0)],
1283 };
1284 assert!(matches!(scope.resolve_path(&path), Err(PathError::Shape(_))));
1285 }
1286
1287 #[test]
1288 fn unset_dynamic_key_is_undefined_root_not_absence() {
1289 let mut scope = Scope::new();
1292 let r = subscripted(
1293 &mut scope,
1294 "r",
1295 serde_json::json!({"name": "amy"}),
1296 VarSegment::Dynamic("k".into()),
1297 );
1298 assert!(matches!(r, Err(PathError::UndefinedRoot(_))), "got: {r:?}");
1299 }
1300
1301 #[test]
1302 fn contains_finds_variable() {
1303 let mut scope = Scope::new();
1304 scope.set("EXISTS", Value::Bool(true));
1305 assert!(scope.contains("EXISTS"));
1306 assert!(!scope.contains("MISSING"));
1307 }
1308
1309 #[test]
1310 fn all_names_lists_variables() {
1311 let mut scope = Scope::new();
1312 scope.set("A", Value::Int(1));
1313 scope.set("B", Value::Int(2));
1314 scope.push_frame();
1315 scope.set("C", Value::Int(3));
1316
1317 let names = scope.all_names();
1318 assert!(names.contains(&"A"));
1319 assert!(names.contains(&"B"));
1320 assert!(names.contains(&"C"));
1321 }
1322
1323 #[test]
1324 #[should_panic(expected = "cannot pop the root scope frame")]
1325 fn pop_root_frame_panics() {
1326 let mut scope = Scope::new();
1327 scope.pop_frame();
1328 }
1329
1330 #[test]
1331 fn positional_params_basic() {
1332 let mut scope = Scope::new();
1333 scope.set_positional("my_tool", vec!["arg1".into(), "arg2".into(), "arg3".into()]);
1334
1335 assert_eq!(scope.get_positional(0), Some("my_tool"));
1337 assert_eq!(scope.get_positional(1), Some("arg1"));
1339 assert_eq!(scope.get_positional(2), Some("arg2"));
1340 assert_eq!(scope.get_positional(3), Some("arg3"));
1341 assert_eq!(scope.get_positional(4), None);
1343 }
1344
1345 #[test]
1346 fn positional_params_empty() {
1347 let scope = Scope::new();
1348 assert_eq!(scope.get_positional(0), None);
1350 assert_eq!(scope.get_positional(1), None);
1351 assert_eq!(scope.arg_count(), 0);
1352 assert!(scope.all_args().is_empty());
1353 }
1354
1355 #[test]
1356 fn all_args_returns_slice() {
1357 let mut scope = Scope::new();
1358 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1359
1360 let args = scope.all_args();
1361 assert_eq!(args, &["a", "b", "c"]);
1362 }
1363
1364 #[test]
1365 fn arg_count_returns_count() {
1366 let mut scope = Scope::new();
1367 scope.set_positional("test", vec!["one".into(), "two".into()]);
1368
1369 assert_eq!(scope.arg_count(), 2);
1370 }
1371
1372 #[test]
1373 fn export_marks_variable() {
1374 let mut scope = Scope::new();
1375 scope.set("X", Value::Int(42));
1376
1377 assert!(!scope.is_exported("X"));
1378 scope.export("X");
1379 assert!(scope.is_exported("X"));
1380 }
1381
1382 #[test]
1383 fn set_exported_sets_and_exports() {
1384 let mut scope = Scope::new();
1385 scope.set_exported("PATH", Value::String("/usr/bin".into()));
1386
1387 assert!(scope.is_exported("PATH"));
1388 assert_eq!(scope.get("PATH"), Some(&Value::String("/usr/bin".into())));
1389 }
1390
1391 #[test]
1392 fn unexport_removes_export_marker() {
1393 let mut scope = Scope::new();
1394 scope.set_exported("VAR", Value::Int(1));
1395 assert!(scope.is_exported("VAR"));
1396
1397 scope.unexport("VAR");
1398 assert!(!scope.is_exported("VAR"));
1399 assert!(scope.get("VAR").is_some());
1401 }
1402
1403 #[test]
1404 fn exported_vars_returns_only_exported_with_values() {
1405 let mut scope = Scope::new();
1406 scope.set_exported("A", Value::Int(1));
1407 scope.set_exported("B", Value::Int(2));
1408 scope.set("C", Value::Int(3)); scope.export("D"); let exported = scope.exported_vars();
1412 assert_eq!(exported.len(), 2);
1413 assert_eq!(exported[0], ("A".to_string(), Value::Int(1)));
1414 assert_eq!(exported[1], ("B".to_string(), Value::Int(2)));
1415 }
1416
1417 #[test]
1418 fn exported_names_returns_sorted_names() {
1419 let mut scope = Scope::new();
1420 scope.export("Z");
1421 scope.export("A");
1422 scope.export("M");
1423
1424 let names = scope.exported_names();
1425 assert_eq!(names, vec!["A", "M", "Z"]);
1426 }
1427
1428 fn write_at(
1432 scope: &mut Scope,
1433 root: &str,
1434 segs: Vec<VarSegment>,
1435 ) -> Result<(), PathError> {
1436 let mut segments = vec![VarSegment::Field(root.into())];
1437 segments.extend(segs);
1438 scope.walk_write(&VarPath { segments }, Value::Int(0))
1439 }
1440
1441 #[test]
1442 fn walk_write_list_index_update() {
1443 let mut scope = Scope::new();
1444 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1445 let path = VarPath {
1446 segments: vec![VarSegment::Field("xs".into()), VarSegment::Index(0)],
1447 };
1448 scope.walk_write(&path, Value::Int(9)).expect("write should succeed");
1449 assert_eq!(scope.get("xs"), Some(&Value::Json(serde_json::json!([9, 2, 3]))));
1450 }
1451
1452 #[test]
1453 fn walk_write_negative_index() {
1454 let mut scope = Scope::new();
1455 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1456 let path = VarPath {
1457 segments: vec![VarSegment::Field("xs".into()), VarSegment::Index(-1)],
1458 };
1459 scope.walk_write(&path, Value::Int(7)).expect("write should succeed");
1460 assert_eq!(scope.get("xs"), Some(&Value::Json(serde_json::json!([1, 2, 7]))));
1461 }
1462
1463 #[test]
1464 fn walk_write_inserts_a_new_record_key() {
1465 let mut scope = Scope::new();
1466 scope.set("u", Value::Json(serde_json::json!({"port": 8080})));
1467 let path = VarPath {
1468 segments: vec![VarSegment::Field("u".into()), VarSegment::Key("host".into())],
1469 };
1470 scope
1471 .walk_write(&path, Value::String("localhost".into()))
1472 .expect("write should succeed");
1473 assert_eq!(
1474 scope.get("u"),
1475 Some(&Value::Json(serde_json::json!({"port": 8080, "host": "localhost"})))
1476 );
1477 }
1478
1479 #[test]
1480 fn walk_write_deep_path_updates_nested_key() {
1481 let mut scope = Scope::new();
1482 scope.set("s", Value::Json(serde_json::json!({"web": {"port": 8080}})));
1483 let path = VarPath {
1484 segments: vec![
1485 VarSegment::Field("s".into()),
1486 VarSegment::Key("web".into()),
1487 VarSegment::Key("port".into()),
1488 ],
1489 };
1490 scope.walk_write(&path, Value::Int(9000)).expect("write should succeed");
1491 assert_eq!(
1492 scope.get("s"),
1493 Some(&Value::Json(serde_json::json!({"web": {"port": 9000}})))
1494 );
1495 }
1496
1497 #[test]
1498 fn walk_write_out_of_bounds_index_is_absence() {
1499 let mut scope = Scope::new();
1500 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1501 let r = write_at(&mut scope, "xs", vec![VarSegment::Index(9)]);
1502 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1503 }
1504
1505 #[test]
1506 fn walk_write_missing_intermediate_is_absence_no_autoviv() {
1507 let mut scope = Scope::new();
1508 scope.set("s", Value::Json(serde_json::json!({"web": {"port": 8080}})));
1509 let r = write_at(
1510 &mut scope,
1511 "s",
1512 vec![VarSegment::Key("api".into()), VarSegment::Key("port".into())],
1513 );
1514 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1515 assert_eq!(
1517 scope.get("s"),
1518 Some(&Value::Json(serde_json::json!({"web": {"port": 8080}})))
1519 );
1520 }
1521
1522 #[test]
1523 fn walk_write_scalar_root_is_shape() {
1524 let mut scope = Scope::new();
1525 scope.set("y", Value::String("hi".into()));
1526 let r = write_at(&mut scope, "y", vec![VarSegment::Index(0)]);
1527 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1528 }
1529
1530 #[test]
1531 fn walk_write_undefined_root_is_undefined_root() {
1532 let mut scope = Scope::new();
1533 let r = write_at(&mut scope, "z", vec![VarSegment::Index(0)]);
1534 assert!(matches!(r, Err(PathError::UndefinedRoot(_))), "got: {r:?}");
1535 }
1536
1537 #[test]
1538 fn walk_write_slice_lvalue_is_shape() {
1539 let mut scope = Scope::new();
1540 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1541 let r = write_at(&mut scope, "xs", vec![VarSegment::Slice(Some(0), Some(2))]);
1542 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1543 }
1544
1545 #[test]
1548 fn walk_append_extends_a_list_in_place() {
1549 let mut scope = Scope::new();
1550 scope.set("xs", Value::Json(serde_json::json!(["a", "b"])));
1551 scope
1552 .walk_append(&VarPath::simple("xs"), vec![Value::String("c".into())])
1553 .expect("push should succeed");
1554 assert_eq!(scope.get("xs"), Some(&Value::Json(serde_json::json!(["a", "b", "c"]))));
1555 }
1556
1557 #[test]
1558 fn walk_append_undefined_target_is_a_loud_error() {
1559 let mut scope = Scope::new();
1560 let r = scope.walk_append(&VarPath::simple("nope"), vec![Value::Int(1)]);
1561 assert!(r.is_err(), "expected a loud error for an undefined target");
1562 }
1563
1564 #[test]
1565 fn walk_append_non_list_target_is_a_loud_error() {
1566 let mut scope = Scope::new();
1567 scope.set("y", Value::String("hi".into()));
1568 let r = scope.walk_append(&VarPath::simple("y"), vec![Value::Int(1)]);
1569 assert!(r.is_err(), "expected a loud error for a non-list target");
1570 }
1571
1572 #[test]
1573 fn walk_append_bracket_path_extends_a_nested_list_in_place() {
1574 let mut scope = Scope::new();
1575 scope.set(
1576 "services",
1577 Value::Json(serde_json::json!({"web": {"tags": ["a"]}})),
1578 );
1579 let path = VarPath {
1580 segments: vec![
1581 VarSegment::Field("services".into()),
1582 VarSegment::Key("web".into()),
1583 VarSegment::Key("tags".into()),
1584 ],
1585 };
1586 scope
1587 .walk_append(&path, vec![Value::String("b".into())])
1588 .expect("bracket-path push should succeed");
1589 assert_eq!(
1590 scope.get("services"),
1591 Some(&Value::Json(serde_json::json!({"web": {"tags": ["a", "b"]}})))
1592 );
1593 }
1594
1595 #[test]
1596 fn walk_append_bracket_path_missing_intermediate_is_a_loud_error() {
1597 let mut scope = Scope::new();
1598 scope.set("services", Value::Json(serde_json::json!({})));
1599 let path = VarPath {
1600 segments: vec![
1601 VarSegment::Field("services".into()),
1602 VarSegment::Key("web".into()),
1603 VarSegment::Key("tags".into()),
1604 ],
1605 };
1606 let r = scope.walk_append(&path, vec![Value::String("x".into())]);
1607 assert!(r.is_err(), "expected a loud error for a missing intermediate");
1608 }
1609
1610 #[test]
1611 fn walk_append_bracket_path_non_list_leaf_is_a_loud_error() {
1612 let mut scope = Scope::new();
1613 scope.set(
1614 "services",
1615 Value::Json(serde_json::json!({"web": {"port": 8080}})),
1616 );
1617 let path = VarPath {
1618 segments: vec![
1619 VarSegment::Field("services".into()),
1620 VarSegment::Key("web".into()),
1621 VarSegment::Key("port".into()),
1622 ],
1623 };
1624 let r = scope.walk_append(&path, vec![Value::Int(1)]);
1625 assert!(r.is_err(), "expected a loud error for a non-list leaf");
1626 }
1627}