1use super::state::ShellState;
3
4fn glob_match(pattern: &str, text: &str) -> bool {
7 glob_match_recursive(pattern, text, 0, 0)
8}
9
10fn glob_match_recursive(pattern: &str, text: &str, pi: usize, ti: usize) -> bool {
11 if pi >= pattern.len() {
13 return ti >= text.len();
14 }
15
16 if ti >= text.len() {
18 return pattern[pi..].chars().all(|c| c == '*');
19 }
20
21 match pattern.chars().nth(pi).unwrap() {
22 '*' => {
23 if glob_match_recursive(pattern, text, pi + 1, ti) {
26 return true;
27 }
28 if ti < text.len() {
30 return glob_match_recursive(pattern, text, pi, ti + 1);
31 }
32 false
33 }
34 c => {
35 if c == text.chars().nth(ti).unwrap() {
37 glob_match_recursive(pattern, text, pi + 1, ti + 1)
38 } else {
39 false
40 }
41 }
42 }
43}
44
45fn find_shortest_prefix_match(pattern: &str, text: &str) -> Option<usize> {
47 if pattern.is_empty() {
48 return Some(0);
49 }
50
51 for i in 0..=text.len() {
52 let prefix = &text[..i];
53 if glob_match(pattern, prefix) {
54 return Some(i);
55 }
56 }
57 None
58}
59
60fn find_longest_prefix_match(pattern: &str, text: &str) -> Option<usize> {
62 if pattern.is_empty() {
63 return Some(0);
64 }
65
66 let mut longest = None;
67 for i in 0..=text.len() {
68 let prefix = &text[..i];
69 if glob_match(pattern, prefix) {
70 longest = Some(i);
71 }
72 }
73 longest
74}
75
76fn find_shortest_suffix_match(pattern: &str, text: &str) -> Option<usize> {
78 if pattern.is_empty() {
79 return Some(text.len());
80 }
81
82 for i in (0..=text.len()).rev() {
83 let suffix = &text[i..];
84 if glob_match(pattern, suffix) {
85 return Some(i);
86 }
87 }
88 None
89}
90
91fn find_longest_suffix_match(pattern: &str, text: &str) -> Option<usize> {
93 if pattern.is_empty() {
94 return Some(text.len());
95 }
96
97 let mut longest = None;
98 for i in (0..=text.len()).rev() {
99 let suffix = &text[i..];
100 if glob_match(pattern, suffix) {
101 longest = Some(i);
102 }
103 }
104 longest
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum ParameterModifier {
110 None,
112 Default(String),
114 AssignDefault(String),
116 Alternative(String),
118 Error(String),
120 Substring(usize),
122 SubstringWithLength(usize, usize),
124 RemoveShortestPrefix(String),
126 RemoveLongestPrefix(String),
128 RemoveShortestSuffix(String),
130 RemoveLongestSuffix(String),
132 Substitute(String, String),
134 SubstituteAll(String, String),
136 Indirect,
138 IndirectPrefix,
140 IndirectPrefixAt,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct ParameterExpansion {
147 pub var_name: String,
148 pub modifier: ParameterModifier,
149}
150
151pub fn parse_parameter_expansion(content: &str) -> Result<ParameterExpansion, String> {
153 if content.is_empty() {
154 return Err("Empty parameter expansion".to_string());
155 }
156
157 let chars = content.chars();
158 let mut var_name = String::new();
159
160 for ch in chars {
162 if ch == ':' || ch == '#' || ch == '%' || ch == '/' {
163 let modifier_str: String = content[var_name.len()..].to_string();
165 let modifier = parse_modifier(&modifier_str)?;
166 return Ok(ParameterExpansion { var_name, modifier });
167 } else if ch == '!' {
168 var_name.push(ch);
171 } else if ch.is_alphanumeric() || ch == '_' || ch == '*' {
172 var_name.push(ch);
174 } else {
175 return Err(format!("Invalid character '{}' in variable name", ch));
176 }
177 }
178
179 let (final_var_name, modifier) = if let Some(stripped) = var_name.strip_prefix('!') {
181 if let Some(prefix_var) = stripped.strip_suffix('*') {
182 (
184 prefix_var.to_string(),
185 ParameterModifier::IndirectPrefix,
186 )
187 } else if let Some(prefix_var) = stripped.strip_suffix('@') {
188 (
190 prefix_var.to_string(),
191 ParameterModifier::IndirectPrefixAt,
192 )
193 } else {
194 (
196 stripped.to_string(),
197 ParameterModifier::Indirect,
198 )
199 }
200 } else {
201 (var_name, ParameterModifier::None)
202 };
203
204 Ok(ParameterExpansion {
205 var_name: final_var_name,
206 modifier,
207 })
208}
209
210fn parse_modifier(modifier_str: &str) -> Result<ParameterModifier, String> {
212 if modifier_str.is_empty() {
213 return Ok(ParameterModifier::None);
214 }
215
216 let mut chars = modifier_str.chars();
217
218 match chars.next().unwrap() {
219 ':' => {
220 match chars.next() {
221 Some('=') => {
222 let word = modifier_str[2..].to_string();
224 Ok(ParameterModifier::AssignDefault(word))
225 }
226 Some('-') => {
227 let word = modifier_str[2..].to_string();
229 Ok(ParameterModifier::Default(word))
230 }
231 Some('+') => {
232 let word = modifier_str[2..].to_string();
234 Ok(ParameterModifier::Alternative(word))
235 }
236 Some('?') => {
237 let word = modifier_str[2..].to_string();
239 Ok(ParameterModifier::Error(word))
240 }
241 Some(ch) if ch.is_ascii_digit() => {
242 let after_colon = &modifier_str[1..]; let offset_end = after_colon.find(':').unwrap_or(after_colon.len());
248 let offset_str = &after_colon[..offset_end];
249
250 if offset_str.is_empty() {
251 return Err("Missing offset in substring operation".to_string());
252 }
253
254 let offset: usize = offset_str.parse().map_err(|_| "Invalid offset number")?;
255
256 if offset_end < after_colon.len() {
258 let after_offset = &after_colon[offset_end + 1..]; if !after_offset.is_empty()
261 && after_offset.chars().all(|c| c.is_ascii_digit())
262 {
263 let length: usize =
264 after_offset.parse().map_err(|_| "Invalid length number")?;
265 Ok(ParameterModifier::SubstringWithLength(offset, length))
266 } else {
267 Ok(ParameterModifier::Substring(offset))
268 }
269 } else {
270 Ok(ParameterModifier::Substring(offset))
271 }
272 }
273 _ => Err(format!("Invalid modifier: {}", modifier_str)),
274 }
275 }
276 '#' => {
277 if let Some(pattern) = modifier_str.strip_prefix("##") {
278 Ok(ParameterModifier::RemoveLongestPrefix(pattern.to_string()))
280 } else if let Some(pattern) = modifier_str.strip_prefix('#') {
281 Ok(ParameterModifier::RemoveShortestPrefix(pattern.to_string()))
283 } else {
284 Err(format!("Invalid prefix removal modifier: {}", modifier_str))
285 }
286 }
287 '%' => {
288 if let Some(pattern) = modifier_str.strip_prefix("%%") {
289 Ok(ParameterModifier::RemoveLongestSuffix(pattern.to_string()))
291 } else if let Some(pattern) = modifier_str.strip_prefix('%') {
292 Ok(ParameterModifier::RemoveShortestSuffix(pattern.to_string()))
294 } else {
295 Err(format!("Invalid suffix removal modifier: {}", modifier_str))
296 }
297 }
298 '/' => {
299 let remaining: String = chars.as_str().to_string();
301
302 if modifier_str.starts_with("//") {
303 let after_double_slash = &remaining[1..]; if let Some(slash_pos) = after_double_slash.find('/') {
306 let pattern = after_double_slash[..slash_pos].to_string();
307 let replacement = after_double_slash[slash_pos + 1..].to_string();
308 Ok(ParameterModifier::SubstituteAll(pattern, replacement))
309 } else {
310 Err("Invalid substitution syntax: missing replacement".to_string())
311 }
312 } else {
313 if let Some(slash_pos) = remaining.find('/') {
315 let pattern = remaining[..slash_pos].to_string();
316 let replacement = remaining[slash_pos + 1..].to_string();
317 Ok(ParameterModifier::Substitute(pattern, replacement))
318 } else {
319 Err("Invalid substitution syntax: missing replacement".to_string())
320 }
321 }
322 }
323 '!' => {
324 let prefix = modifier_str[1..].to_string();
325 if prefix.ends_with('*') {
326 Ok(ParameterModifier::IndirectPrefix)
327 } else if prefix.ends_with('@') {
328 Ok(ParameterModifier::IndirectPrefixAt)
329 } else {
330 Err("Invalid indirect expansion: must end with * or @".to_string())
331 }
332 }
333 _ => Err(format!("Unknown modifier: {}", modifier_str)),
334 }
335}
336
337fn collect_variable_names_with_prefix(prefix: &str, shell_state: &ShellState) -> Vec<String> {
339 let mut matching_vars = std::collections::HashSet::new();
340
341 for var_name in shell_state.variables.keys() {
343 if var_name.starts_with(prefix) {
344 matching_vars.insert(var_name.clone());
345 }
346 }
347
348 for scope in &shell_state.local_vars {
350 for var_name in scope.keys() {
351 if var_name.starts_with(prefix) {
352 matching_vars.insert(var_name.clone());
353 }
354 }
355 }
356
357 let mut result: Vec<String> = matching_vars.into_iter().collect();
359 result.sort();
360 result
361}
362
363pub fn expand_parameter(
365 expansion: &ParameterExpansion,
366 shell_state: &ShellState,
367) -> Result<String, String> {
368 let value = match expansion.modifier {
369 ParameterModifier::None => {
370 shell_state.get_var(&expansion.var_name)
372 }
373 ParameterModifier::Indirect => {
374 if let Some(indirect_name) = shell_state.get_var(&expansion.var_name) {
378 shell_state.get_var(&indirect_name)
379 } else {
380 Some("".to_string())
381 }
382 }
383 ParameterModifier::Default(ref default) => {
384 match shell_state.get_var(&expansion.var_name) {
386 Some(val) if !val.is_empty() => Some(val),
387 _ => Some(default.clone()),
388 }
389 }
390 ParameterModifier::AssignDefault(ref default) => {
391 match shell_state.get_var(&expansion.var_name) {
393 Some(val) if !val.is_empty() => Some(val),
394 _ => {
395 Some(default.clone())
397 }
398 }
399 }
400 ParameterModifier::Alternative(ref alternative) => {
401 match shell_state.get_var(&expansion.var_name) {
403 Some(val) if !val.is_empty() => Some(alternative.clone()),
404 _ => Some("".to_string()),
405 }
406 }
407 ParameterModifier::Error(ref error_msg) => {
408 match shell_state.get_var(&expansion.var_name) {
410 Some(val) if !val.is_empty() => Some(val),
411 _ => {
412 let msg = if error_msg.is_empty() {
413 format!("parameter '{}' not set", expansion.var_name)
414 } else {
415 error_msg.clone()
416 };
417 return Err(msg);
418 }
419 }
420 }
421 ParameterModifier::Substring(offset) => {
422 if let Some(val) = shell_state.get_var(&expansion.var_name) {
424 let start = offset.min(val.len());
425 Some(val[start..].to_string())
426 } else {
427 Some("".to_string())
428 }
429 }
430 ParameterModifier::SubstringWithLength(offset, length) => {
431 if let Some(val) = shell_state.get_var(&expansion.var_name) {
433 let start = offset.min(val.len());
434 let end = (start + length).min(val.len());
435 Some(val[start..end].to_string())
436 } else {
437 Some("".to_string())
438 }
439 }
440 ParameterModifier::RemoveShortestPrefix(ref pattern) => {
441 if let Some(val) = shell_state.get_var(&expansion.var_name) {
443 if let Some(match_end) = find_shortest_prefix_match(pattern, &val) {
444 Some(val[match_end..].to_string())
445 } else {
446 Some(val.clone())
447 }
448 } else {
449 Some("".to_string())
450 }
451 }
452 ParameterModifier::RemoveLongestPrefix(ref pattern) => {
453 if let Some(val) = shell_state.get_var(&expansion.var_name) {
455 if let Some(match_end) = find_longest_prefix_match(pattern, &val) {
456 Some(val[match_end..].to_string())
457 } else {
458 Some(val.clone())
459 }
460 } else {
461 Some("".to_string())
462 }
463 }
464 ParameterModifier::RemoveShortestSuffix(ref pattern) => {
465 if let Some(val) = shell_state.get_var(&expansion.var_name) {
467 if let Some(match_start) = find_shortest_suffix_match(pattern, &val) {
468 Some(val[..match_start].to_string())
469 } else {
470 Some(val.clone())
471 }
472 } else {
473 Some("".to_string())
474 }
475 }
476 ParameterModifier::RemoveLongestSuffix(ref pattern) => {
477 if let Some(val) = shell_state.get_var(&expansion.var_name) {
479 if let Some(match_start) = find_longest_suffix_match(pattern, &val) {
480 Some(val[..match_start].to_string())
481 } else {
482 Some(val.clone())
483 }
484 } else {
485 Some("".to_string())
486 }
487 }
488 ParameterModifier::Substitute(ref pattern, ref replacement) => {
489 if let Some(val) = shell_state.get_var(&expansion.var_name) {
491 Some(val.replace(pattern, replacement))
493 } else {
494 Some("".to_string())
495 }
496 }
497 ParameterModifier::SubstituteAll(ref pattern, ref replacement) => {
498 if let Some(val) = shell_state.get_var(&expansion.var_name) {
500 Some(val.replace(pattern, replacement))
502 } else {
503 Some("".to_string())
504 }
505 }
506 ParameterModifier::IndirectPrefix | ParameterModifier::IndirectPrefixAt => {
507 let matching_vars = collect_variable_names_with_prefix(&expansion.var_name, shell_state);
509 Some(matching_vars.join(" "))
510 }
511 };
512
513 Ok(value.unwrap_or_else(|| "".to_string()))
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_parse_simple_variable() {
522 let result = parse_parameter_expansion("VAR").unwrap();
523 assert_eq!(result.var_name, "VAR");
524 assert_eq!(result.modifier, ParameterModifier::None);
525 }
526
527 #[test]
528 fn test_parse_default_modifier() {
529 let result = parse_parameter_expansion("VAR:-default").unwrap();
530 assert_eq!(result.var_name, "VAR");
531 assert_eq!(
532 result.modifier,
533 ParameterModifier::Default("default".to_string())
534 );
535 }
536
537 #[test]
538 fn test_parse_assign_default_modifier() {
539 let result = parse_parameter_expansion("VAR:=default").unwrap();
540 assert_eq!(result.var_name, "VAR");
541 assert_eq!(
542 result.modifier,
543 ParameterModifier::AssignDefault("default".to_string())
544 );
545 }
546
547 #[test]
548 fn test_parse_alternative_modifier() {
549 let result = parse_parameter_expansion("VAR:+alt").unwrap();
550 assert_eq!(result.var_name, "VAR");
551 assert_eq!(
552 result.modifier,
553 ParameterModifier::Alternative("alt".to_string())
554 );
555 }
556
557 #[test]
558 fn test_parse_error_modifier() {
559 let result = parse_parameter_expansion("VAR:?error").unwrap();
560 assert_eq!(result.var_name, "VAR");
561 assert_eq!(
562 result.modifier,
563 ParameterModifier::Error("error".to_string())
564 );
565 }
566
567 #[test]
568 fn test_parse_substring() {
569 let result = parse_parameter_expansion("VAR:5").unwrap();
570 assert_eq!(result.var_name, "VAR");
571 assert_eq!(result.modifier, ParameterModifier::Substring(5));
572 }
573
574 #[test]
575 fn test_parse_substring_with_length() {
576 let result = parse_parameter_expansion("VAR:2:3").unwrap();
577 assert_eq!(result.var_name, "VAR");
578 assert_eq!(
579 result.modifier,
580 ParameterModifier::SubstringWithLength(2, 3)
581 );
582 }
583
584 #[test]
585 fn test_parse_remove_shortest_prefix() {
586 let result = parse_parameter_expansion("VAR#prefix").unwrap();
587 assert_eq!(result.var_name, "VAR");
588 assert_eq!(
589 result.modifier,
590 ParameterModifier::RemoveShortestPrefix("prefix".to_string())
591 );
592 }
593
594 #[test]
595 fn test_parse_remove_longest_prefix() {
596 let result = parse_parameter_expansion("VAR##prefix").unwrap();
597 assert_eq!(result.var_name, "VAR");
598 assert_eq!(
599 result.modifier,
600 ParameterModifier::RemoveLongestPrefix("prefix".to_string())
601 );
602 }
603
604 #[test]
605 fn test_parse_remove_shortest_suffix() {
606 let result = parse_parameter_expansion("VAR%suffix").unwrap();
607 assert_eq!(result.var_name, "VAR");
608 assert_eq!(
609 result.modifier,
610 ParameterModifier::RemoveShortestSuffix("suffix".to_string())
611 );
612 }
613
614 #[test]
615 fn test_parse_remove_longest_suffix() {
616 let result = parse_parameter_expansion("VAR%%suffix").unwrap();
617 assert_eq!(result.var_name, "VAR");
618 assert_eq!(
619 result.modifier,
620 ParameterModifier::RemoveLongestSuffix("suffix".to_string())
621 );
622 }
623
624 #[test]
625 fn test_parse_substitute() {
626 let result = parse_parameter_expansion("VAR/old/new").unwrap();
627 assert_eq!(result.var_name, "VAR");
628 assert_eq!(
629 result.modifier,
630 ParameterModifier::Substitute("old".to_string(), "new".to_string())
631 );
632 }
633
634 #[test]
635 fn test_parse_substitute_all() {
636 let result = parse_parameter_expansion("VAR//old/new").unwrap();
637 assert_eq!(result.var_name, "VAR");
638 assert_eq!(
639 result.modifier,
640 ParameterModifier::SubstituteAll("old".to_string(), "new".to_string())
641 );
642 }
643
644 #[test]
645 fn test_parse_indirect_prefix() {
646 let result = parse_parameter_expansion("!PREFIX*").unwrap();
647 assert_eq!(result.var_name, "PREFIX");
648 assert_eq!(result.modifier, ParameterModifier::IndirectPrefix);
649 }
650
651 #[test]
652 fn test_parse_empty() {
653 let result = parse_parameter_expansion("");
654 assert!(result.is_err());
655 }
656
657 #[test]
658 fn test_parse_invalid_character() {
659 let result = parse_parameter_expansion("VAR@test");
660 assert!(result.is_err());
661 }
662
663 #[test]
664 fn test_expand_simple_variable() {
665 let mut shell_state = ShellState::new();
666 shell_state.set_var("TEST_VAR", "hello world".to_string());
667
668 let expansion = ParameterExpansion {
669 var_name: "TEST_VAR".to_string(),
670 modifier: ParameterModifier::None,
671 };
672
673 let result = expand_parameter(&expansion, &shell_state).unwrap();
674 assert_eq!(result, "hello world");
675 }
676
677 #[test]
678 fn test_expand_default_modifier() {
679 let mut shell_state = ShellState::new();
680 shell_state.set_var("TEST_VAR", "value".to_string());
681
682 let expansion = ParameterExpansion {
683 var_name: "TEST_VAR".to_string(),
684 modifier: ParameterModifier::Default("default".to_string()),
685 };
686
687 let result = expand_parameter(&expansion, &shell_state).unwrap();
688 assert_eq!(result, "value");
689 }
690
691 #[test]
692 fn test_expand_default_modifier_unset() {
693 let shell_state = ShellState::new();
694
695 let expansion = ParameterExpansion {
696 var_name: "UNSET_VAR".to_string(),
697 modifier: ParameterModifier::Default("default".to_string()),
698 };
699
700 let result = expand_parameter(&expansion, &shell_state).unwrap();
701 assert_eq!(result, "default");
702 }
703
704 #[test]
705 fn test_expand_substring() {
706 let mut shell_state = ShellState::new();
707 shell_state.set_var("TEST_VAR", "hello world".to_string());
708
709 let expansion = ParameterExpansion {
710 var_name: "TEST_VAR".to_string(),
711 modifier: ParameterModifier::Substring(6),
712 };
713
714 let result = expand_parameter(&expansion, &shell_state).unwrap();
715 assert_eq!(result, "world");
716 }
717
718 #[test]
719 fn test_expand_indirect_prefix_basic() {
720 let mut shell_state = ShellState::new();
721 shell_state.set_var("MY_VAR1", "value1".to_string());
722 shell_state.set_var("MY_VAR2", "value2".to_string());
723 shell_state.set_var("OTHER_VAR", "other".to_string());
724 shell_state.set_var("MY_PREFIX_VAR", "prefix".to_string());
725
726 let expansion = ParameterExpansion {
727 var_name: "MY_".to_string(),
728 modifier: ParameterModifier::IndirectPrefix,
729 };
730
731 let result = expand_parameter(&expansion, &shell_state).unwrap();
732 assert_eq!(result, "MY_PREFIX_VAR MY_VAR1 MY_VAR2");
734 }
735
736 #[test]
737 fn test_expand_indirect_prefix_with_locals() {
738 let mut shell_state = ShellState::new();
739
740 shell_state.set_var("GLOBAL_VAR", "global".to_string());
742 shell_state.set_var("TEST_VAR1", "test1".to_string());
743
744 shell_state.push_local_scope();
746 shell_state.set_local_var("LOCAL_VAR", "local".to_string());
747 shell_state.set_local_var("TEST_VAR2", "test2".to_string());
748
749 let expansion = ParameterExpansion {
750 var_name: "TEST_".to_string(),
751 modifier: ParameterModifier::IndirectPrefix,
752 };
753
754 let result = expand_parameter(&expansion, &shell_state).unwrap();
755 assert_eq!(result, "TEST_VAR1 TEST_VAR2");
757 }
758
759 #[test]
760 fn test_expand_indirect_prefix_no_matches() {
761 let mut shell_state = ShellState::new();
762 shell_state.set_var("VAR1", "value1".to_string());
763 shell_state.set_var("VAR2", "value2".to_string());
764
765 let expansion = ParameterExpansion {
766 var_name: "NONEXISTENT_".to_string(),
767 modifier: ParameterModifier::IndirectPrefix,
768 };
769
770 let result = expand_parameter(&expansion, &shell_state).unwrap();
771 assert_eq!(result, "");
773 }
774
775 #[test]
776 fn test_expand_indirect_prefix_empty_prefix() {
777 let mut shell_state = ShellState::new();
778 shell_state.set_var("VAR1", "value1".to_string());
779 shell_state.set_var("VAR2", "value2".to_string());
780 shell_state.set_var("ANOTHER_VAR", "another".to_string());
781
782 let expansion = ParameterExpansion {
783 var_name: "".to_string(),
784 modifier: ParameterModifier::IndirectPrefix,
785 };
786
787 let result = expand_parameter(&expansion, &shell_state).unwrap();
788 assert_eq!(result, "ANOTHER_VAR VAR1 VAR2");
790 }
791
792 #[test]
793 fn test_expand_indirect_prefix_at() {
794 let mut shell_state = ShellState::new();
795 shell_state.set_var("PREFIX_VAR1", "value1".to_string());
796 shell_state.set_var("PREFIX_VAR2", "value2".to_string());
797
798 let expansion = ParameterExpansion {
799 var_name: "PREFIX_".to_string(),
800 modifier: ParameterModifier::IndirectPrefixAt,
801 };
802
803 let result = expand_parameter(&expansion, &shell_state).unwrap();
804 assert_eq!(result, "PREFIX_VAR1 PREFIX_VAR2");
806 }
807
808 #[test]
809 fn test_expand_indirect_prefix_mixed_scopes() {
810 let mut shell_state = ShellState::new();
811
812 shell_state.set_var("APP_CONFIG", "global_config".to_string());
814 shell_state.set_var("APP_DEBUG", "false".to_string());
815
816 shell_state.push_local_scope();
818 shell_state.set_local_var("APP_TEMP", "temp_value".to_string());
819
820 shell_state.push_local_scope();
822 shell_state.set_local_var("APP_SECRET", "secret_value".to_string());
823
824 let expansion = ParameterExpansion {
825 var_name: "APP_".to_string(),
826 modifier: ParameterModifier::IndirectPrefix,
827 };
828
829 let result = expand_parameter(&expansion, &shell_state).unwrap();
830 assert_eq!(result, "APP_CONFIG APP_DEBUG APP_SECRET APP_TEMP");
832 }
833
834 #[test]
835 fn test_expand_indirect_prefix_special_characters() {
836 let mut shell_state = ShellState::new();
837 shell_state.set_var("TEST-VAR", "dash".to_string());
838 shell_state.set_var("TEST.VAR", "dot".to_string());
839 shell_state.set_var("TEST_VAR", "underscore".to_string());
840
841 let expansion = ParameterExpansion {
842 var_name: "TEST".to_string(),
843 modifier: ParameterModifier::IndirectPrefix,
844 };
845
846 let result = expand_parameter(&expansion, &shell_state).unwrap();
847 assert_eq!(result, "TEST-VAR TEST.VAR TEST_VAR");
849 }
850
851 #[test]
852 fn test_parse_indirect_basic() {
853 let result = parse_parameter_expansion("!VAR_NAME").unwrap();
854 assert_eq!(result.var_name, "VAR_NAME");
855 assert_eq!(result.modifier, ParameterModifier::Indirect);
856 }
857
858 #[test]
859 fn test_expand_indirect_basic() {
860 let mut shell_state = ShellState::new();
861 shell_state.set_var("VAR_NAME", "TARGET_VAR".to_string());
862 shell_state.set_var("TARGET_VAR", "final_value".to_string());
863
864 let expansion = ParameterExpansion {
865 var_name: "VAR_NAME".to_string(),
866 modifier: ParameterModifier::Indirect,
867 };
868
869 let result = expand_parameter(&expansion, &shell_state).unwrap();
870 assert_eq!(result, "final_value");
872 }
873
874 #[test]
875 fn test_expand_indirect_basic_unset_intermediate() {
876 let mut shell_state = ShellState::new();
877 shell_state.set_var("TARGET_VAR", "final_value".to_string());
878 let expansion = ParameterExpansion {
881 var_name: "VAR_NAME".to_string(),
882 modifier: ParameterModifier::Indirect,
883 };
884
885 let result = expand_parameter(&expansion, &shell_state).unwrap();
886 assert_eq!(result, "");
888 }
889
890 #[test]
891 fn test_expand_indirect_basic_unset_target() {
892 let mut shell_state = ShellState::new();
893 shell_state.set_var("VAR_NAME", "NONEXISTENT".to_string());
894 let expansion = ParameterExpansion {
897 var_name: "VAR_NAME".to_string(),
898 modifier: ParameterModifier::Indirect,
899 };
900
901 let result = expand_parameter(&expansion, &shell_state).unwrap();
902 assert_eq!(result, "");
904 }
905
906 #[test]
907 fn test_expand_indirect_basic_with_local_scope() {
908 let mut shell_state = ShellState::new();
909
910 shell_state.set_var("VAR_NAME", "GLOBAL_TARGET".to_string());
912 shell_state.set_var("GLOBAL_TARGET", "global_value".to_string());
913
914 shell_state.push_local_scope();
916 shell_state.set_local_var("VAR_NAME", "LOCAL_TARGET".to_string());
917 shell_state.set_local_var("LOCAL_TARGET", "local_value".to_string());
918
919 let expansion = ParameterExpansion {
920 var_name: "VAR_NAME".to_string(),
921 modifier: ParameterModifier::Indirect,
922 };
923
924 let result = expand_parameter(&expansion, &shell_state).unwrap();
925 assert_eq!(result, "local_value");
927 }
928}