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 (prefix_var.to_string(), ParameterModifier::IndirectPrefix)
184 } else if let Some(prefix_var) = stripped.strip_suffix('@') {
185 (prefix_var.to_string(), ParameterModifier::IndirectPrefixAt)
187 } else {
188 (stripped.to_string(), ParameterModifier::Indirect)
190 }
191 } else {
192 (var_name, ParameterModifier::None)
193 };
194
195 Ok(ParameterExpansion {
196 var_name: final_var_name,
197 modifier,
198 })
199}
200
201fn parse_modifier(modifier_str: &str) -> Result<ParameterModifier, String> {
203 if modifier_str.is_empty() {
204 return Ok(ParameterModifier::None);
205 }
206
207 let mut chars = modifier_str.chars();
208
209 match chars.next().unwrap() {
210 ':' => {
211 match chars.next() {
212 Some('=') => {
213 let word = modifier_str[2..].to_string();
215 Ok(ParameterModifier::AssignDefault(word))
216 }
217 Some('-') => {
218 let word = modifier_str[2..].to_string();
220 Ok(ParameterModifier::Default(word))
221 }
222 Some('+') => {
223 let word = modifier_str[2..].to_string();
225 Ok(ParameterModifier::Alternative(word))
226 }
227 Some('?') => {
228 let word = modifier_str[2..].to_string();
230 Ok(ParameterModifier::Error(word))
231 }
232 Some(ch) if ch.is_ascii_digit() => {
233 let after_colon = &modifier_str[1..]; let offset_end = after_colon.find(':').unwrap_or(after_colon.len());
239 let offset_str = &after_colon[..offset_end];
240
241 if offset_str.is_empty() {
242 return Err("Missing offset in substring operation".to_string());
243 }
244
245 let offset: usize = offset_str.parse().map_err(|_| "Invalid offset number")?;
246
247 if offset_end < after_colon.len() {
249 let after_offset = &after_colon[offset_end + 1..]; if !after_offset.is_empty()
252 && after_offset.chars().all(|c| c.is_ascii_digit())
253 {
254 let length: usize =
255 after_offset.parse().map_err(|_| "Invalid length number")?;
256 Ok(ParameterModifier::SubstringWithLength(offset, length))
257 } else {
258 Ok(ParameterModifier::Substring(offset))
259 }
260 } else {
261 Ok(ParameterModifier::Substring(offset))
262 }
263 }
264 _ => Err(format!("Invalid modifier: {}", modifier_str)),
265 }
266 }
267 '#' => {
268 if let Some(pattern) = modifier_str.strip_prefix("##") {
269 Ok(ParameterModifier::RemoveLongestPrefix(pattern.to_string()))
271 } else if let Some(pattern) = modifier_str.strip_prefix('#') {
272 Ok(ParameterModifier::RemoveShortestPrefix(pattern.to_string()))
274 } else {
275 Err(format!("Invalid prefix removal modifier: {}", modifier_str))
276 }
277 }
278 '%' => {
279 if let Some(pattern) = modifier_str.strip_prefix("%%") {
280 Ok(ParameterModifier::RemoveLongestSuffix(pattern.to_string()))
282 } else if let Some(pattern) = modifier_str.strip_prefix('%') {
283 Ok(ParameterModifier::RemoveShortestSuffix(pattern.to_string()))
285 } else {
286 Err(format!("Invalid suffix removal modifier: {}", modifier_str))
287 }
288 }
289 '/' => {
290 let remaining: String = chars.as_str().to_string();
292
293 if modifier_str.starts_with("//") {
294 let after_double_slash = &remaining[1..]; if let Some(slash_pos) = after_double_slash.find('/') {
297 let pattern = after_double_slash[..slash_pos].to_string();
298 let replacement = after_double_slash[slash_pos + 1..].to_string();
299 Ok(ParameterModifier::SubstituteAll(pattern, replacement))
300 } else {
301 Err("Invalid substitution syntax: missing replacement".to_string())
302 }
303 } else {
304 if let Some(slash_pos) = remaining.find('/') {
306 let pattern = remaining[..slash_pos].to_string();
307 let replacement = remaining[slash_pos + 1..].to_string();
308 Ok(ParameterModifier::Substitute(pattern, replacement))
309 } else {
310 Err("Invalid substitution syntax: missing replacement".to_string())
311 }
312 }
313 }
314 '!' => {
315 let prefix = modifier_str[1..].to_string();
316 if prefix.ends_with('*') {
317 Ok(ParameterModifier::IndirectPrefix)
318 } else if prefix.ends_with('@') {
319 Ok(ParameterModifier::IndirectPrefixAt)
320 } else {
321 Err("Invalid indirect expansion: must end with * or @".to_string())
322 }
323 }
324 _ => Err(format!("Unknown modifier: {}", modifier_str)),
325 }
326}
327
328fn collect_variable_names_with_prefix(prefix: &str, shell_state: &ShellState) -> Vec<String> {
330 let mut matching_vars = std::collections::HashSet::new();
331
332 for var_name in shell_state.variables.keys() {
334 if var_name.starts_with(prefix) {
335 matching_vars.insert(var_name.clone());
336 }
337 }
338
339 for scope in &shell_state.local_vars {
341 for var_name in scope.keys() {
342 if var_name.starts_with(prefix) {
343 matching_vars.insert(var_name.clone());
344 }
345 }
346 }
347
348 let mut result: Vec<String> = matching_vars.into_iter().collect();
350 result.sort();
351 result
352}
353
354pub fn expand_parameter(
372 expansion: &ParameterExpansion,
373 shell_state: &ShellState,
374) -> Result<String, String> {
375 let value = match expansion.modifier {
376 ParameterModifier::None => {
377 let var_value = shell_state.get_var(&expansion.var_name);
379
380 if shell_state.options.nounset && var_value.is_none() {
382 return Err(format!("{}: unbound variable", expansion.var_name));
383 }
384
385 var_value
386 }
387 ParameterModifier::Indirect => {
388 if let Some(indirect_name) = shell_state.get_var(&expansion.var_name) {
392 shell_state.get_var(&indirect_name)
393 } else {
394 Some("".to_string())
395 }
396 }
397 ParameterModifier::Default(ref default) => {
398 match shell_state.get_var(&expansion.var_name) {
400 Some(val) if !val.is_empty() => Some(val),
401 _ => Some(default.clone()),
402 }
403 }
404 ParameterModifier::AssignDefault(ref default) => {
405 match shell_state.get_var(&expansion.var_name) {
407 Some(val) if !val.is_empty() => Some(val),
408 _ => {
409 Some(default.clone())
411 }
412 }
413 }
414 ParameterModifier::Alternative(ref alternative) => {
415 match shell_state.get_var(&expansion.var_name) {
417 Some(val) if !val.is_empty() => Some(alternative.clone()),
418 _ => Some("".to_string()),
419 }
420 }
421 ParameterModifier::Error(ref error_msg) => {
422 match shell_state.get_var(&expansion.var_name) {
424 Some(val) if !val.is_empty() => Some(val),
425 _ => {
426 let msg = if error_msg.is_empty() {
427 format!("parameter '{}' not set", expansion.var_name)
428 } else {
429 error_msg.clone()
430 };
431 return Err(msg);
432 }
433 }
434 }
435 ParameterModifier::Substring(offset) => {
436 if let Some(val) = shell_state.get_var(&expansion.var_name) {
438 let start = offset.min(val.len());
439 Some(val[start..].to_string())
440 } else {
441 Some("".to_string())
442 }
443 }
444 ParameterModifier::SubstringWithLength(offset, length) => {
445 if let Some(val) = shell_state.get_var(&expansion.var_name) {
447 let start = offset.min(val.len());
448 let end = (start + length).min(val.len());
449 Some(val[start..end].to_string())
450 } else {
451 Some("".to_string())
452 }
453 }
454 ParameterModifier::RemoveShortestPrefix(ref pattern) => {
455 if let Some(val) = shell_state.get_var(&expansion.var_name) {
457 if let Some(match_end) = find_shortest_prefix_match(pattern, &val) {
458 Some(val[match_end..].to_string())
459 } else {
460 Some(val.clone())
461 }
462 } else {
463 Some("".to_string())
464 }
465 }
466 ParameterModifier::RemoveLongestPrefix(ref pattern) => {
467 if let Some(val) = shell_state.get_var(&expansion.var_name) {
469 if let Some(match_end) = find_longest_prefix_match(pattern, &val) {
470 Some(val[match_end..].to_string())
471 } else {
472 Some(val.clone())
473 }
474 } else {
475 Some("".to_string())
476 }
477 }
478 ParameterModifier::RemoveShortestSuffix(ref pattern) => {
479 if let Some(val) = shell_state.get_var(&expansion.var_name) {
481 if let Some(match_start) = find_shortest_suffix_match(pattern, &val) {
482 Some(val[..match_start].to_string())
483 } else {
484 Some(val.clone())
485 }
486 } else {
487 Some("".to_string())
488 }
489 }
490 ParameterModifier::RemoveLongestSuffix(ref pattern) => {
491 if let Some(val) = shell_state.get_var(&expansion.var_name) {
493 if let Some(match_start) = find_longest_suffix_match(pattern, &val) {
494 Some(val[..match_start].to_string())
495 } else {
496 Some(val.clone())
497 }
498 } else {
499 Some("".to_string())
500 }
501 }
502 ParameterModifier::Substitute(ref pattern, ref replacement) => {
503 if let Some(val) = shell_state.get_var(&expansion.var_name) {
505 Some(val.replace(pattern, replacement))
507 } else {
508 Some("".to_string())
509 }
510 }
511 ParameterModifier::SubstituteAll(ref pattern, ref replacement) => {
512 if let Some(val) = shell_state.get_var(&expansion.var_name) {
514 Some(val.replace(pattern, replacement))
516 } else {
517 Some("".to_string())
518 }
519 }
520 ParameterModifier::IndirectPrefix | ParameterModifier::IndirectPrefixAt => {
521 let matching_vars =
523 collect_variable_names_with_prefix(&expansion.var_name, shell_state);
524 Some(matching_vars.join(" "))
525 }
526 };
527
528 Ok(value.unwrap_or_else(|| "".to_string()))
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn test_parse_simple_variable() {
537 let result = parse_parameter_expansion("VAR").unwrap();
538 assert_eq!(result.var_name, "VAR");
539 assert_eq!(result.modifier, ParameterModifier::None);
540 }
541
542 #[test]
543 fn test_parse_default_modifier() {
544 let result = parse_parameter_expansion("VAR:-default").unwrap();
545 assert_eq!(result.var_name, "VAR");
546 assert_eq!(
547 result.modifier,
548 ParameterModifier::Default("default".to_string())
549 );
550 }
551
552 #[test]
553 fn test_parse_assign_default_modifier() {
554 let result = parse_parameter_expansion("VAR:=default").unwrap();
555 assert_eq!(result.var_name, "VAR");
556 assert_eq!(
557 result.modifier,
558 ParameterModifier::AssignDefault("default".to_string())
559 );
560 }
561
562 #[test]
563 fn test_parse_alternative_modifier() {
564 let result = parse_parameter_expansion("VAR:+alt").unwrap();
565 assert_eq!(result.var_name, "VAR");
566 assert_eq!(
567 result.modifier,
568 ParameterModifier::Alternative("alt".to_string())
569 );
570 }
571
572 #[test]
573 fn test_parse_error_modifier() {
574 let result = parse_parameter_expansion("VAR:?error").unwrap();
575 assert_eq!(result.var_name, "VAR");
576 assert_eq!(
577 result.modifier,
578 ParameterModifier::Error("error".to_string())
579 );
580 }
581
582 #[test]
583 fn test_parse_substring() {
584 let result = parse_parameter_expansion("VAR:5").unwrap();
585 assert_eq!(result.var_name, "VAR");
586 assert_eq!(result.modifier, ParameterModifier::Substring(5));
587 }
588
589 #[test]
590 fn test_parse_substring_with_length() {
591 let result = parse_parameter_expansion("VAR:2:3").unwrap();
592 assert_eq!(result.var_name, "VAR");
593 assert_eq!(
594 result.modifier,
595 ParameterModifier::SubstringWithLength(2, 3)
596 );
597 }
598
599 #[test]
600 fn test_parse_remove_shortest_prefix() {
601 let result = parse_parameter_expansion("VAR#prefix").unwrap();
602 assert_eq!(result.var_name, "VAR");
603 assert_eq!(
604 result.modifier,
605 ParameterModifier::RemoveShortestPrefix("prefix".to_string())
606 );
607 }
608
609 #[test]
610 fn test_parse_remove_longest_prefix() {
611 let result = parse_parameter_expansion("VAR##prefix").unwrap();
612 assert_eq!(result.var_name, "VAR");
613 assert_eq!(
614 result.modifier,
615 ParameterModifier::RemoveLongestPrefix("prefix".to_string())
616 );
617 }
618
619 #[test]
620 fn test_parse_remove_shortest_suffix() {
621 let result = parse_parameter_expansion("VAR%suffix").unwrap();
622 assert_eq!(result.var_name, "VAR");
623 assert_eq!(
624 result.modifier,
625 ParameterModifier::RemoveShortestSuffix("suffix".to_string())
626 );
627 }
628
629 #[test]
630 fn test_parse_remove_longest_suffix() {
631 let result = parse_parameter_expansion("VAR%%suffix").unwrap();
632 assert_eq!(result.var_name, "VAR");
633 assert_eq!(
634 result.modifier,
635 ParameterModifier::RemoveLongestSuffix("suffix".to_string())
636 );
637 }
638
639 #[test]
640 fn test_parse_substitute() {
641 let result = parse_parameter_expansion("VAR/old/new").unwrap();
642 assert_eq!(result.var_name, "VAR");
643 assert_eq!(
644 result.modifier,
645 ParameterModifier::Substitute("old".to_string(), "new".to_string())
646 );
647 }
648
649 #[test]
650 fn test_parse_substitute_all() {
651 let result = parse_parameter_expansion("VAR//old/new").unwrap();
652 assert_eq!(result.var_name, "VAR");
653 assert_eq!(
654 result.modifier,
655 ParameterModifier::SubstituteAll("old".to_string(), "new".to_string())
656 );
657 }
658
659 #[test]
660 fn test_parse_indirect_prefix() {
661 let result = parse_parameter_expansion("!PREFIX*").unwrap();
662 assert_eq!(result.var_name, "PREFIX");
663 assert_eq!(result.modifier, ParameterModifier::IndirectPrefix);
664 }
665
666 #[test]
667 fn test_parse_empty() {
668 let result = parse_parameter_expansion("");
669 assert!(result.is_err());
670 }
671
672 #[test]
673 fn test_parse_invalid_character() {
674 let result = parse_parameter_expansion("VAR@test");
675 assert!(result.is_err());
676 }
677
678 #[test]
679 fn test_expand_simple_variable() {
680 let mut shell_state = ShellState::new();
681 shell_state.set_var("TEST_VAR", "hello world".to_string());
682
683 let expansion = ParameterExpansion {
684 var_name: "TEST_VAR".to_string(),
685 modifier: ParameterModifier::None,
686 };
687
688 let result = expand_parameter(&expansion, &shell_state).unwrap();
689 assert_eq!(result, "hello world");
690 }
691
692 #[test]
693 fn test_expand_default_modifier() {
694 let mut shell_state = ShellState::new();
695 shell_state.set_var("TEST_VAR", "value".to_string());
696
697 let expansion = ParameterExpansion {
698 var_name: "TEST_VAR".to_string(),
699 modifier: ParameterModifier::Default("default".to_string()),
700 };
701
702 let result = expand_parameter(&expansion, &shell_state).unwrap();
703 assert_eq!(result, "value");
704 }
705
706 #[test]
707 fn test_expand_default_modifier_unset() {
708 let shell_state = ShellState::new();
709
710 let expansion = ParameterExpansion {
711 var_name: "UNSET_VAR".to_string(),
712 modifier: ParameterModifier::Default("default".to_string()),
713 };
714
715 let result = expand_parameter(&expansion, &shell_state).unwrap();
716 assert_eq!(result, "default");
717 }
718
719 #[test]
720 fn test_expand_substring() {
721 let mut shell_state = ShellState::new();
722 shell_state.set_var("TEST_VAR", "hello world".to_string());
723
724 let expansion = ParameterExpansion {
725 var_name: "TEST_VAR".to_string(),
726 modifier: ParameterModifier::Substring(6),
727 };
728
729 let result = expand_parameter(&expansion, &shell_state).unwrap();
730 assert_eq!(result, "world");
731 }
732
733 #[test]
734 fn test_expand_indirect_prefix_basic() {
735 let mut shell_state = ShellState::new();
736 shell_state.set_var("MY_VAR1", "value1".to_string());
737 shell_state.set_var("MY_VAR2", "value2".to_string());
738 shell_state.set_var("OTHER_VAR", "other".to_string());
739 shell_state.set_var("MY_PREFIX_VAR", "prefix".to_string());
740
741 let expansion = ParameterExpansion {
742 var_name: "MY_".to_string(),
743 modifier: ParameterModifier::IndirectPrefix,
744 };
745
746 let result = expand_parameter(&expansion, &shell_state).unwrap();
747 assert_eq!(result, "MY_PREFIX_VAR MY_VAR1 MY_VAR2");
749 }
750
751 #[test]
752 fn test_expand_indirect_prefix_with_locals() {
753 let mut shell_state = ShellState::new();
754
755 shell_state.set_var("GLOBAL_VAR", "global".to_string());
757 shell_state.set_var("TEST_VAR1", "test1".to_string());
758
759 shell_state.push_local_scope();
761 shell_state.set_local_var("LOCAL_VAR", "local".to_string());
762 shell_state.set_local_var("TEST_VAR2", "test2".to_string());
763
764 let expansion = ParameterExpansion {
765 var_name: "TEST_".to_string(),
766 modifier: ParameterModifier::IndirectPrefix,
767 };
768
769 let result = expand_parameter(&expansion, &shell_state).unwrap();
770 assert_eq!(result, "TEST_VAR1 TEST_VAR2");
772 }
773
774 #[test]
775 fn test_expand_indirect_prefix_no_matches() {
776 let mut shell_state = ShellState::new();
777 shell_state.set_var("VAR1", "value1".to_string());
778 shell_state.set_var("VAR2", "value2".to_string());
779
780 let expansion = ParameterExpansion {
781 var_name: "NONEXISTENT_".to_string(),
782 modifier: ParameterModifier::IndirectPrefix,
783 };
784
785 let result = expand_parameter(&expansion, &shell_state).unwrap();
786 assert_eq!(result, "");
788 }
789
790 #[test]
791 fn test_expand_indirect_prefix_empty_prefix() {
792 let mut shell_state = ShellState::new();
793 shell_state.set_var("VAR1", "value1".to_string());
794 shell_state.set_var("VAR2", "value2".to_string());
795 shell_state.set_var("ANOTHER_VAR", "another".to_string());
796
797 let expansion = ParameterExpansion {
798 var_name: "".to_string(),
799 modifier: ParameterModifier::IndirectPrefix,
800 };
801
802 let result = expand_parameter(&expansion, &shell_state).unwrap();
803 assert_eq!(result, "ANOTHER_VAR VAR1 VAR2");
805 }
806
807 #[test]
808 fn test_expand_indirect_prefix_at() {
809 let mut shell_state = ShellState::new();
810 shell_state.set_var("PREFIX_VAR1", "value1".to_string());
811 shell_state.set_var("PREFIX_VAR2", "value2".to_string());
812
813 let expansion = ParameterExpansion {
814 var_name: "PREFIX_".to_string(),
815 modifier: ParameterModifier::IndirectPrefixAt,
816 };
817
818 let result = expand_parameter(&expansion, &shell_state).unwrap();
819 assert_eq!(result, "PREFIX_VAR1 PREFIX_VAR2");
821 }
822
823 #[test]
824 fn test_expand_indirect_prefix_mixed_scopes() {
825 let mut shell_state = ShellState::new();
826
827 shell_state.set_var("APP_CONFIG", "global_config".to_string());
829 shell_state.set_var("APP_DEBUG", "false".to_string());
830
831 shell_state.push_local_scope();
833 shell_state.set_local_var("APP_TEMP", "temp_value".to_string());
834
835 shell_state.push_local_scope();
837 shell_state.set_local_var("APP_SECRET", "secret_value".to_string());
838
839 let expansion = ParameterExpansion {
840 var_name: "APP_".to_string(),
841 modifier: ParameterModifier::IndirectPrefix,
842 };
843
844 let result = expand_parameter(&expansion, &shell_state).unwrap();
845 assert_eq!(result, "APP_CONFIG APP_DEBUG APP_SECRET APP_TEMP");
847 }
848
849 #[test]
850 fn test_expand_indirect_prefix_special_characters() {
851 let mut shell_state = ShellState::new();
852 shell_state.set_var("TEST-VAR", "dash".to_string());
853 shell_state.set_var("TEST.VAR", "dot".to_string());
854 shell_state.set_var("TEST_VAR", "underscore".to_string());
855
856 let expansion = ParameterExpansion {
857 var_name: "TEST".to_string(),
858 modifier: ParameterModifier::IndirectPrefix,
859 };
860
861 let result = expand_parameter(&expansion, &shell_state).unwrap();
862 assert_eq!(result, "TEST-VAR TEST.VAR TEST_VAR");
864 }
865
866 #[test]
867 fn test_parse_indirect_basic() {
868 let result = parse_parameter_expansion("!VAR_NAME").unwrap();
869 assert_eq!(result.var_name, "VAR_NAME");
870 assert_eq!(result.modifier, ParameterModifier::Indirect);
871 }
872
873 #[test]
874 fn test_expand_indirect_basic() {
875 let mut shell_state = ShellState::new();
876 shell_state.set_var("VAR_NAME", "TARGET_VAR".to_string());
877 shell_state.set_var("TARGET_VAR", "final_value".to_string());
878
879 let expansion = ParameterExpansion {
880 var_name: "VAR_NAME".to_string(),
881 modifier: ParameterModifier::Indirect,
882 };
883
884 let result = expand_parameter(&expansion, &shell_state).unwrap();
885 assert_eq!(result, "final_value");
887 }
888
889 #[test]
890 fn test_expand_indirect_basic_unset_intermediate() {
891 let mut shell_state = ShellState::new();
892 shell_state.set_var("TARGET_VAR", "final_value".to_string());
893 let expansion = ParameterExpansion {
896 var_name: "VAR_NAME".to_string(),
897 modifier: ParameterModifier::Indirect,
898 };
899
900 let result = expand_parameter(&expansion, &shell_state).unwrap();
901 assert_eq!(result, "");
903 }
904
905 #[test]
906 fn test_expand_indirect_basic_unset_target() {
907 let mut shell_state = ShellState::new();
908 shell_state.set_var("VAR_NAME", "NONEXISTENT".to_string());
909 let expansion = ParameterExpansion {
912 var_name: "VAR_NAME".to_string(),
913 modifier: ParameterModifier::Indirect,
914 };
915
916 let result = expand_parameter(&expansion, &shell_state).unwrap();
917 assert_eq!(result, "");
919 }
920
921 #[test]
922 fn test_expand_indirect_basic_with_local_scope() {
923 let mut shell_state = ShellState::new();
924
925 shell_state.set_var("VAR_NAME", "GLOBAL_TARGET".to_string());
927 shell_state.set_var("GLOBAL_TARGET", "global_value".to_string());
928
929 shell_state.push_local_scope();
931 shell_state.set_local_var("VAR_NAME", "LOCAL_TARGET".to_string());
932 shell_state.set_local_var("LOCAL_TARGET", "local_value".to_string());
933
934 let expansion = ParameterExpansion {
935 var_name: "VAR_NAME".to_string(),
936 modifier: ParameterModifier::Indirect,
937 };
938
939 let result = expand_parameter(&expansion, &shell_state).unwrap();
940 assert_eq!(result, "local_value");
942 }
943}