1use std::cell::Cell;
7
8use indexmap::IndexMap;
9use smol_str::SmolStr;
10
11const NOUNSET_ERROR_VAR: &str = "_NOUNSET_ERROR";
16
17#[derive(Debug, Clone, PartialEq)]
19pub enum VarValue {
20 Scalar(SmolStr),
21 IndexedArray(IndexMap<usize, SmolStr>),
22 AssocArray(IndexMap<SmolStr, SmolStr>),
23}
24
25impl VarValue {
26 #[must_use]
30 pub fn as_scalar(&self) -> SmolStr {
31 match self {
32 Self::Scalar(s) => s.clone(),
33 Self::IndexedArray(map) => {
34 let vals: Vec<&str> = map.values().map(SmolStr::as_str).collect();
35 SmolStr::from(vals.join(" "))
36 }
37 Self::AssocArray(map) => {
38 let vals: Vec<&str> = map.values().map(SmolStr::as_str).collect();
39 SmolStr::from(vals.join(" "))
40 }
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq)]
47#[allow(clippy::struct_excessive_bools)]
48pub struct ShellVar {
49 pub value: VarValue,
50 pub exported: bool,
51 pub readonly: bool,
52 pub integer: bool,
54 pub nameref: bool,
56}
57
58impl ShellVar {
59 pub fn scalar(value: SmolStr) -> Self {
61 Self {
62 value: VarValue::Scalar(value),
63 exported: false,
64 readonly: false,
65 integer: false,
66 nameref: false,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct ShellEnv {
74 pub scopes: Vec<IndexMap<SmolStr, ShellVar>>,
75}
76
77impl ShellEnv {
78 #[must_use]
79 pub fn new() -> Self {
80 Self {
81 scopes: vec![IndexMap::new()],
82 }
83 }
84
85 #[must_use]
87 pub fn get(&self, name: &str) -> Option<&ShellVar> {
88 for scope in self.scopes.iter().rev() {
89 if let Some(var) = scope.get(name) {
90 return Some(var);
91 }
92 }
93 None
94 }
95
96 pub fn get_mut(&mut self, name: &str) -> Option<&mut ShellVar> {
98 for scope in self.scopes.iter_mut().rev() {
99 if let Some(var) = scope.get_mut(name) {
100 return Some(var);
101 }
102 }
103 None
104 }
105
106 pub fn set(&mut self, name: SmolStr, var: ShellVar) {
108 if let Some(scope) = self.scopes.last_mut() {
109 scope.insert(name, var);
110 }
111 }
112
113 pub fn push_scope(&mut self) {
115 self.scopes.push(IndexMap::new());
116 }
117
118 pub fn pop_scope(&mut self) -> Option<IndexMap<SmolStr, ShellVar>> {
120 if self.scopes.len() > 1 {
121 self.scopes.pop()
122 } else {
123 None
124 }
125 }
126
127 pub fn remove(&mut self, name: &str) -> Option<ShellVar> {
129 if let Some(scope) = self.scopes.last_mut() {
130 scope.shift_remove(name)
131 } else {
132 None
133 }
134 }
135
136 pub fn exported_vars(&self) -> IndexMap<SmolStr, SmolStr> {
139 let mut result = IndexMap::new();
140 for scope in &self.scopes {
141 for (name, var) in scope {
142 if var.exported {
143 result.insert(name.clone(), var.value.as_scalar());
144 }
145 }
146 }
147 result
148 }
149}
150
151impl Default for ShellEnv {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct ShellState {
160 pub env: ShellEnv,
162 pub positional: Vec<SmolStr>,
164 pub last_status: i32,
166 pub cwd: String,
168 pub lineno: u32,
170 pub random_seed: Cell<u32>,
172 #[cfg(not(target_arch = "wasm32"))]
174 pub start_time: std::time::Instant,
175 pub func_stack: Vec<SmolStr>,
177 pub source_stack: Vec<SmolStr>,
179 pub script_name: Option<SmolStr>,
182 pub shell_pid: u32,
184 pub last_background_pid: Option<u32>,
186 pub last_argument: SmolStr,
188 pub dir_stack: Vec<SmolStr>,
190 pub umask: u32,
192}
193
194impl ShellState {
195 #[must_use]
196 pub fn new() -> Self {
197 Self {
198 env: ShellEnv::new(),
199 positional: Vec::new(),
200 last_status: 0,
201 cwd: "/".into(),
202 lineno: 0,
203 random_seed: Cell::new(12345),
204 #[cfg(not(target_arch = "wasm32"))]
205 start_time: std::time::Instant::now(),
206 func_stack: Vec::new(),
207 source_stack: Vec::new(),
208 script_name: None,
209 shell_pid: shell_pid(),
210 last_background_pid: None,
211 last_argument: SmolStr::default(),
212 dir_stack: Vec::new(),
213 umask: 0o022,
214 }
215 }
216
217 fn next_random(&self) -> u32 {
219 let mut x = self.random_seed.get();
220 x ^= x << 13;
221 x ^= x >> 17;
222 x ^= x << 5;
223 self.random_seed.set(x);
224 x % 32768
225 }
226
227 #[must_use]
232 pub fn get_var(&self, name: &str) -> Option<SmolStr> {
233 match name {
234 "?" => Some(self.last_status.to_string().into()),
235 "$$" => Some(self.shell_pid.to_string().into()),
236 "!" => Some(
237 self.last_background_pid
238 .map_or_else(SmolStr::default, |pid| pid.to_string().into()),
239 ),
240 "#" => Some(self.positional.len().to_string().into()),
241 "-" => Some(self.option_flags()),
242 "0" => Some(self.script_name.clone().unwrap_or_else(|| "wasmsh".into())),
243 "_" => Some(self.last_argument.clone()),
244 "@" | "*" => Some(self.positional.join(" ").into()),
245 "RANDOM" => Some(SmolStr::from(self.next_random().to_string())),
246 "LINENO" => Some(SmolStr::from(self.lineno.to_string())),
247 "SECONDS" => Some(self.seconds_value()),
248 "FUNCNAME" => Some(stack_last_or_default(&self.func_stack)),
249 "BASH_SOURCE" => Some(stack_last_or_default(&self.source_stack)),
250 _ => self.get_named_or_positional_var(name),
251 }
252 }
253
254 #[allow(clippy::unused_self)] fn seconds_value(&self) -> SmolStr {
256 #[cfg(not(target_arch = "wasm32"))]
257 {
258 SmolStr::from(self.start_time.elapsed().as_secs().to_string())
259 }
260 #[cfg(target_arch = "wasm32")]
261 {
262 SmolStr::from("0")
263 }
264 }
265
266 fn option_flags(&self) -> SmolStr {
267 let mut flags = String::new();
268 for (name, flag) in [
269 ("SHOPT_a", 'a'),
270 ("SHOPT_C", 'C'),
271 ("SHOPT_E", 'E'),
272 ("SHOPT_e", 'e'),
273 ("SHOPT_f", 'f'),
274 ("SHOPT_n", 'n'),
275 ("SHOPT_p", 'p'),
276 ("SHOPT_T", 'T'),
277 ("SHOPT_u", 'u'),
278 ("SHOPT_v", 'v'),
279 ("SHOPT_x", 'x'),
280 ] {
281 if self.get_env_var(name).as_deref() == Some("1") {
282 flags.push(flag);
283 }
284 }
285 flags.into()
286 }
287
288 fn get_named_or_positional_var(&self, name: &str) -> Option<SmolStr> {
289 if let Some(index) = positional_param_index(name) {
290 return self.positional.get(index).cloned();
291 }
292 self.get_env_var(name)
293 }
294
295 fn get_env_var(&self, name: &str) -> Option<SmolStr> {
296 let var = self.env.get(name)?;
297 if let Some(target) = nameref_target(var, name) {
298 return self.env.get(&target).map(|v| v.value.as_scalar());
299 }
300 Some(var.value.as_scalar())
301 }
302
303 pub fn set_last_argument(&mut self, arg: impl Into<SmolStr>) {
305 self.last_argument = arg.into();
306 }
307
308 pub fn set_last_background_pid(&mut self, pid: Option<u32>) {
310 self.last_background_pid = pid;
311 }
312
313 pub fn set_var(&mut self, name: SmolStr, value: SmolStr) {
319 if let Some(var) = self.env.get(&name) {
321 if var.nameref {
322 let target = var.value.as_scalar();
323 if !target.is_empty() && target.as_str() != name.as_str() {
324 let target_name = SmolStr::from(target.as_str());
325 self.set_var(target_name, value);
326 return;
327 }
328 }
329 }
330 let (exported, readonly) = self
331 .env
332 .get(&name)
333 .map_or((false, false), |v| (v.exported, v.readonly));
334 if readonly {
335 return; }
337 let exported = if !exported
339 && !name.starts_with("SHOPT_")
340 && !name.starts_with('_')
341 && self
342 .env
343 .get("SHOPT_a")
344 .is_some_and(|v| matches!(&v.value, VarValue::Scalar(s) if s == "1"))
345 {
346 true
347 } else {
348 exported
349 };
350 let (integer, nameref) = self
352 .env
353 .get(&name)
354 .map_or((false, false), |v| (v.integer, v.nameref));
355 self.env.set(
356 name,
357 ShellVar {
358 value: VarValue::Scalar(value),
359 exported,
360 readonly,
361 integer,
362 nameref,
363 },
364 );
365 }
366
367 pub fn set_var_checked(&mut self, name: SmolStr, value: SmolStr) -> Result<(), String> {
369 if let Some(var) = self.env.get(&name) {
370 if var.readonly {
371 return Err(format!("{name}: readonly variable"));
372 }
373 }
374 self.set_var(name, value);
375 Ok(())
376 }
377
378 pub fn set_readonly(&mut self, name: SmolStr, value: SmolStr) {
380 let (exported, integer, nameref) = self.env.get(&name).map_or((false, false, false), |v| {
381 (v.exported, v.integer, v.nameref)
382 });
383 self.env.set(
384 name,
385 ShellVar {
386 value: VarValue::Scalar(value),
387 exported,
388 readonly: true,
389 integer,
390 nameref,
391 },
392 );
393 }
394
395 pub fn unset_var(&mut self, name: &str) -> Result<(), String> {
397 if let Some(var) = self.env.get(name) {
398 if var.readonly {
399 return Err(format!("{name}: readonly variable"));
400 }
401 }
402 self.env.remove(name);
403 Ok(())
404 }
405
406 pub fn set_nounset_error(&mut self, name: &str) {
411 self.env.set(
412 SmolStr::from(NOUNSET_ERROR_VAR),
413 ShellVar::scalar(SmolStr::from(name)),
414 );
415 }
416
417 pub fn take_nounset_error(&mut self) -> Option<SmolStr> {
420 let name = self.env.get(NOUNSET_ERROR_VAR)?.value.as_scalar();
421 if name.is_empty() {
422 return None;
423 }
424 self.env.remove(NOUNSET_ERROR_VAR);
425 Some(name)
426 }
427
428 #[must_use]
431 pub fn var_names_with_prefix(&self, prefix: &str) -> Vec<SmolStr> {
432 let mut seen = IndexMap::<SmolStr, ()>::new();
433 for scope in self.env.scopes.iter().rev() {
435 for name in scope.keys() {
436 if name.starts_with(prefix) {
437 seen.entry(name.clone()).or_default();
438 }
439 }
440 }
441 let mut names: Vec<SmolStr> = seen.into_keys().collect();
442 names.sort();
443 names
444 }
445
446 #[must_use]
453 pub fn get_array_element(&self, name: &str, index: &str) -> Option<SmolStr> {
454 let var = self.env.get(name)?;
455 match &var.value {
456 VarValue::Scalar(s) => {
457 if index == "0" {
458 Some(s.clone())
459 } else {
460 None
461 }
462 }
463 VarValue::IndexedArray(map) => {
464 let idx: usize = index.parse().ok()?;
465 map.get(&idx).cloned()
466 }
467 VarValue::AssocArray(map) => map.get(index).cloned(),
468 }
469 }
470
471 pub fn set_array_element(&mut self, name: SmolStr, index: &str, value: SmolStr) {
474 let (exported, readonly) = self
475 .env
476 .get(&name)
477 .map_or((false, false), |v| (v.exported, v.readonly));
478 if readonly {
479 return;
480 }
481
482 if let Some(var) = self.env.get_mut(&name) {
483 match &mut var.value {
484 VarValue::IndexedArray(map) => {
485 if let Ok(idx) = index.parse::<usize>() {
486 map.insert(idx, value);
487 }
488 }
489 VarValue::AssocArray(map) => {
490 map.insert(SmolStr::from(index), value);
491 }
492 VarValue::Scalar(_) => {
493 let mut map = IndexMap::new();
495 if let Ok(idx) = index.parse::<usize>() {
496 map.insert(idx, value);
497 }
498 var.value = VarValue::IndexedArray(map);
499 }
500 }
501 } else {
502 let mut map = IndexMap::new();
504 if let Ok(idx) = index.parse::<usize>() {
505 map.insert(idx, value);
506 }
507 self.env.set(
508 name,
509 ShellVar {
510 value: VarValue::IndexedArray(map),
511 exported,
512 readonly,
513 integer: false,
514 nameref: false,
515 },
516 );
517 }
518 }
519
520 #[must_use]
522 pub fn get_array_keys(&self, name: &str) -> Vec<String> {
523 let Some(var) = self.env.get(name) else {
524 return Vec::new();
525 };
526 match &var.value {
527 VarValue::Scalar(s) => {
528 if s.is_empty() {
529 Vec::new()
530 } else {
531 vec!["0".to_string()]
532 }
533 }
534 VarValue::IndexedArray(map) => map.keys().map(ToString::to_string).collect(),
535 VarValue::AssocArray(map) => map.keys().map(ToString::to_string).collect(),
536 }
537 }
538
539 #[must_use]
541 pub fn get_array_values(&self, name: &str) -> Vec<SmolStr> {
542 let Some(var) = self.env.get(name) else {
543 return Vec::new();
544 };
545 match &var.value {
546 VarValue::Scalar(s) => {
547 if s.is_empty() {
548 Vec::new()
549 } else {
550 vec![s.clone()]
551 }
552 }
553 VarValue::IndexedArray(map) => map.values().cloned().collect(),
554 VarValue::AssocArray(map) => map.values().cloned().collect(),
555 }
556 }
557
558 #[must_use]
560 pub fn get_array_length(&self, name: &str) -> usize {
561 let Some(var) = self.env.get(name) else {
562 return 0;
563 };
564 match &var.value {
565 VarValue::Scalar(s) => usize::from(!s.is_empty()),
566 VarValue::IndexedArray(map) => map.len(),
567 VarValue::AssocArray(map) => map.len(),
568 }
569 }
570
571 pub fn append_array(&mut self, name: &str, values: Vec<SmolStr>) {
575 if self.env.get(name).is_some_and(|var| var.readonly) {
576 return;
577 }
578
579 let name_key = SmolStr::from(name);
580 if let Some(var) = self.env.get_mut(name) {
581 match &mut var.value {
582 VarValue::IndexedArray(map) => append_to_indexed_array(map, values),
583 VarValue::AssocArray(_) => {
584 }
587 VarValue::Scalar(s) => {
588 var.value = VarValue::IndexedArray(scalar_to_indexed_array(s, values));
589 }
590 }
591 } else {
592 self.env.set(
593 name_key,
594 ShellVar {
595 value: VarValue::IndexedArray(values_to_indexed_array(values)),
596 exported: false,
597 readonly: false,
598 integer: false,
599 nameref: false,
600 },
601 );
602 }
603 }
604
605 pub fn unset_array_element(&mut self, name: &str, index: &str) {
607 if let Some(var) = self.env.get(name) {
608 if var.readonly {
609 return;
610 }
611 }
612 if let Some(var) = self.env.get_mut(name) {
613 match &mut var.value {
614 VarValue::IndexedArray(map) => {
615 if let Ok(idx) = index.parse::<usize>() {
616 map.shift_remove(&idx);
617 }
618 }
619 VarValue::AssocArray(map) => {
620 map.shift_remove(index);
621 }
622 VarValue::Scalar(_) => {
623 if index == "0" {
624 var.value = VarValue::Scalar(SmolStr::default());
625 }
626 }
627 }
628 }
629 }
630
631 pub fn init_indexed_array(&mut self, name: SmolStr) {
633 let (exported, readonly) = self
634 .env
635 .get(&name)
636 .map_or((false, false), |v| (v.exported, v.readonly));
637 if readonly {
638 return;
639 }
640 self.env.set(
641 name,
642 ShellVar {
643 value: VarValue::IndexedArray(IndexMap::new()),
644 exported,
645 readonly,
646 integer: false,
647 nameref: false,
648 },
649 );
650 }
651
652 pub fn init_assoc_array(&mut self, name: SmolStr) {
654 let (exported, readonly) = self
655 .env
656 .get(&name)
657 .map_or((false, false), |v| (v.exported, v.readonly));
658 if readonly {
659 return;
660 }
661 self.env.set(
662 name,
663 ShellVar {
664 value: VarValue::AssocArray(IndexMap::new()),
665 exported,
666 readonly,
667 integer: false,
668 nameref: false,
669 },
670 );
671 }
672}
673
674fn stack_last_or_default(stack: &[SmolStr]) -> SmolStr {
675 stack.last().cloned().unwrap_or_default()
676}
677
678fn shell_pid() -> u32 {
679 #[cfg(not(target_arch = "wasm32"))]
680 {
681 std::process::id()
682 }
683 #[cfg(target_arch = "wasm32")]
684 {
685 1
686 }
687}
688
689fn positional_param_index(name: &str) -> Option<usize> {
690 let n = name.parse::<usize>().ok()?;
691 (n >= 1).then_some(n - 1)
692}
693
694fn nameref_target(var: &ShellVar, name: &str) -> Option<SmolStr> {
695 if !var.nameref {
696 return None;
697 }
698 let target = var.value.as_scalar();
699 (!target.is_empty() && target.as_str() != name).then_some(target)
700}
701
702fn append_to_indexed_array(map: &mut IndexMap<usize, SmolStr>, values: Vec<SmolStr>) {
703 let next = next_array_index(map);
704 for (i, value) in values.into_iter().enumerate() {
705 map.insert(next + i, value);
706 }
707}
708
709fn scalar_to_indexed_array(scalar: &SmolStr, values: Vec<SmolStr>) -> IndexMap<usize, SmolStr> {
710 let mut map = IndexMap::new();
711 if !scalar.is_empty() {
712 map.insert(0, scalar.clone());
713 }
714 append_to_indexed_array(&mut map, values);
715 map
716}
717
718fn values_to_indexed_array(values: Vec<SmolStr>) -> IndexMap<usize, SmolStr> {
719 let mut map = IndexMap::new();
720 append_to_indexed_array(&mut map, values);
721 map
722}
723
724fn next_array_index(map: &IndexMap<usize, SmolStr>) -> usize {
725 map.keys().max().map_or(0, |k| k + 1)
726}
727
728impl Default for ShellState {
729 fn default() -> Self {
730 Self::new()
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737
738 #[test]
739 fn get_set_variable() {
740 let mut state = ShellState::new();
741 state.set_var("FOO".into(), "bar".into());
742 assert_eq!(state.get_var("FOO").unwrap(), "bar");
743 }
744
745 #[test]
746 fn special_params() {
747 let mut state = ShellState::new();
748 state.last_status = 42;
749 assert_eq!(state.get_var("?").unwrap(), "42");
750 assert_eq!(state.get_var("#").unwrap(), "0");
751 assert_eq!(state.get_var("0").unwrap(), "wasmsh");
752 assert_eq!(state.get_var("$$").unwrap(), state.shell_pid.to_string());
753 assert_eq!(state.get_var("!").unwrap(), "");
754 assert_eq!(state.get_var("-").unwrap(), "");
755 assert_eq!(state.get_var("_").unwrap(), "");
756 }
757
758 #[test]
759 fn special_params_track_last_argument_and_options() {
760 let mut state = ShellState::new();
761 state.set_last_argument("world");
762 state.set_var("SHOPT_e".into(), "1".into());
763 state.set_var("SHOPT_u".into(), "1".into());
764 assert_eq!(state.get_var("_").unwrap(), "world");
765 assert_eq!(state.get_var("-").unwrap(), "eu");
766 }
767
768 #[test]
769 fn special_params_track_background_pid() {
770 let mut state = ShellState::new();
771 state.set_last_background_pid(Some(1234));
772 assert_eq!(state.get_var("!").unwrap(), "1234");
773 }
774
775 #[test]
776 fn positional_params() {
777 let mut state = ShellState::new();
778 state.positional = vec!["a".into(), "b".into(), "c".into()];
779 assert_eq!(state.get_var("1").unwrap(), "a");
780 assert_eq!(state.get_var("2").unwrap(), "b");
781 assert_eq!(state.get_var("3").unwrap(), "c");
782 assert!(state.get_var("4").is_none());
783 assert_eq!(state.get_var("#").unwrap(), "3");
784 }
785
786 #[test]
787 fn scope_shadowing() {
788 let mut state = ShellState::new();
789 state.set_var("X".into(), "global".into());
790 state.env.push_scope();
791 state.set_var("X".into(), "local".into());
792 assert_eq!(state.get_var("X").unwrap(), "local");
793 state.env.pop_scope();
794 assert_eq!(state.get_var("X").unwrap(), "global");
795 }
796
797 #[test]
798 fn exported_vars() {
799 let mut state = ShellState::new();
800 state.env.set(
801 "PATH".into(),
802 ShellVar {
803 value: VarValue::Scalar("/bin".into()),
804 exported: true,
805 readonly: false,
806 integer: false,
807 nameref: false,
808 },
809 );
810 state.set_var("LOCAL".into(), "val".into());
811 let exports = state.env.exported_vars();
812 assert_eq!(exports.len(), 1);
813 assert_eq!(exports["PATH"], "/bin");
814 }
815
816 #[test]
817 fn unset_var_removes() {
818 let mut state = ShellState::new();
819 state.set_var("X".into(), "val".into());
820 assert!(state.get_var("X").is_some());
821 state.unset_var("X").unwrap();
822 assert!(state.get_var("X").is_none());
823 }
824
825 #[test]
826 fn nounset_error_roundtrip() {
827 let mut state = ShellState::new();
828 assert!(state.take_nounset_error().is_none());
829
830 state.set_nounset_error("FOO");
831 assert_eq!(state.take_nounset_error().as_deref(), Some("FOO"));
832 assert!(state.take_nounset_error().is_none());
834 }
835
836 #[test]
837 fn readonly_prevents_set() {
838 let mut state = ShellState::new();
839 state.set_readonly("X".into(), "locked".into());
840 assert!(state.set_var_checked("X".into(), "new".into()).is_err());
841 assert_eq!(state.get_var("X").unwrap(), "locked");
842 }
843
844 #[test]
845 fn readonly_prevents_unset() {
846 let mut state = ShellState::new();
847 state.set_readonly("X".into(), "locked".into());
848 assert!(state.unset_var("X").is_err());
849 assert!(state.get_var("X").is_some());
850 }
851
852 #[test]
853 fn set_var_preserves_exported_flag() {
854 let mut state = ShellState::new();
855 state.env.set(
856 "X".into(),
857 ShellVar {
858 value: VarValue::Scalar("old".into()),
859 exported: true,
860 readonly: false,
861 integer: false,
862 nameref: false,
863 },
864 );
865 state.set_var("X".into(), "new".into());
866 let var = state.env.get("X").unwrap();
867 assert_eq!(var.value.as_scalar(), "new");
868 assert!(var.exported); }
870
871 #[test]
874 fn indexed_array_basics() {
875 let mut state = ShellState::new();
876 state.init_indexed_array("arr".into());
877 state.set_array_element("arr".into(), "0", "zero".into());
878 state.set_array_element("arr".into(), "1", "one".into());
879 state.set_array_element("arr".into(), "2", "two".into());
880
881 assert_eq!(state.get_array_element("arr", "0").unwrap(), "zero");
882 assert_eq!(state.get_array_element("arr", "1").unwrap(), "one");
883 assert_eq!(state.get_array_element("arr", "2").unwrap(), "two");
884 assert!(state.get_array_element("arr", "3").is_none());
885
886 assert_eq!(state.get_array_length("arr"), 3);
887 assert_eq!(state.get_array_keys("arr"), vec!["0", "1", "2"]);
888 assert_eq!(
889 state.get_array_values("arr"),
890 vec![
891 SmolStr::from("zero"),
892 SmolStr::from("one"),
893 SmolStr::from("two")
894 ]
895 );
896 }
897
898 #[test]
899 fn assoc_array_basics() {
900 let mut state = ShellState::new();
901 state.init_assoc_array("map".into());
902 state.set_array_element("map".into(), "key1", "val1".into());
903 state.set_array_element("map".into(), "key2", "val2".into());
904
905 assert_eq!(state.get_array_element("map", "key1").unwrap(), "val1");
906 assert_eq!(state.get_array_element("map", "key2").unwrap(), "val2");
907 assert!(state.get_array_element("map", "key3").is_none());
908
909 assert_eq!(state.get_array_length("map"), 2);
910 }
911
912 #[test]
913 fn array_scalar_access() {
914 let mut state = ShellState::new();
915 state.init_indexed_array("arr".into());
916 state.set_array_element("arr".into(), "0", "a".into());
917 state.set_array_element("arr".into(), "1", "b".into());
918 assert_eq!(state.get_var("arr").unwrap(), "a b");
920 }
921
922 #[test]
923 fn append_array_values() {
924 let mut state = ShellState::new();
925 state.init_indexed_array("arr".into());
926 state.set_array_element("arr".into(), "0", "a".into());
927 state.append_array("arr", vec!["b".into(), "c".into()]);
928 assert_eq!(state.get_array_length("arr"), 3);
929 assert_eq!(state.get_array_element("arr", "1").unwrap(), "b");
930 assert_eq!(state.get_array_element("arr", "2").unwrap(), "c");
931 }
932
933 #[test]
934 fn unset_array_element_removes() {
935 let mut state = ShellState::new();
936 state.init_indexed_array("arr".into());
937 state.set_array_element("arr".into(), "0", "a".into());
938 state.set_array_element("arr".into(), "1", "b".into());
939 state.unset_array_element("arr", "0");
940 assert!(state.get_array_element("arr", "0").is_none());
941 assert_eq!(state.get_array_element("arr", "1").unwrap(), "b");
942 assert_eq!(state.get_array_length("arr"), 1);
943 }
944
945 #[test]
946 fn scalar_as_array_element_0() {
947 let mut state = ShellState::new();
948 state.set_var("X".into(), "hello".into());
949 assert_eq!(state.get_array_element("X", "0").unwrap(), "hello");
951 assert!(state.get_array_element("X", "1").is_none());
952 }
953
954 #[test]
955 fn set_element_creates_indexed_array() {
956 let mut state = ShellState::new();
957 state.set_array_element("arr".into(), "5", "five".into());
958 assert_eq!(state.get_array_element("arr", "5").unwrap(), "five");
959 assert_eq!(state.get_array_length("arr"), 1);
960 }
961
962 #[test]
965 fn random_returns_bounded_value() {
966 let state = ShellState::new();
967 let val: u32 = state.get_var("RANDOM").unwrap().parse().unwrap();
968 assert!(val < 32768);
969 }
970
971 #[test]
972 fn random_changes_each_call() {
973 let state = ShellState::new();
974 let v1 = state.get_var("RANDOM").unwrap();
975 let v2 = state.get_var("RANDOM").unwrap();
976 assert_ne!(v1, v2);
978 }
979
980 #[test]
981 fn lineno_returns_current_value() {
982 let mut state = ShellState::new();
983 state.lineno = 42;
984 assert_eq!(state.get_var("LINENO").unwrap(), "42");
985 }
986
987 #[test]
988 fn seconds_returns_value() {
989 let state = ShellState::new();
990 let val = state.get_var("SECONDS").unwrap();
991 let secs: u64 = val.parse().unwrap();
993 assert!(secs < 60); }
995
996 #[test]
997 fn funcname_empty_by_default() {
998 let state = ShellState::new();
999 assert_eq!(state.get_var("FUNCNAME").unwrap(), "");
1000 }
1001
1002 #[test]
1003 fn funcname_returns_top_of_stack() {
1004 let mut state = ShellState::new();
1005 state.func_stack.push("myfunc".into());
1006 assert_eq!(state.get_var("FUNCNAME").unwrap(), "myfunc");
1007 }
1008
1009 #[test]
1010 fn bash_source_returns_top_of_stack() {
1011 let mut state = ShellState::new();
1012 state.source_stack.push("/script.sh".into());
1013 assert_eq!(state.get_var("BASH_SOURCE").unwrap(), "/script.sh");
1014 }
1015}