rush_sh/
parameter_expansion.rs

1/// Parameter expansion implementation for POSIX sh compatibility
2use super::state::ShellState;
3
4/// Simple glob pattern matcher for POSIX shell parameter expansion
5/// Supports * (matches any sequence of characters) and literal characters
6fn 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 we've consumed both pattern and text, it's a match
12    if pi >= pattern.len() {
13        return ti >= text.len();
14    }
15
16    // If we've consumed text but not pattern, only match if remaining pattern is all *
17    if ti >= text.len() {
18        return pattern[pi..].chars().all(|c| c == '*');
19    }
20
21    match pattern.chars().nth(pi).unwrap() {
22        '*' => {
23            // * matches zero or more characters
24            // Try matching zero characters first, then one, then more
25            if glob_match_recursive(pattern, text, pi + 1, ti) {
26                return true;
27            }
28            // Try matching one more character
29            if ti < text.len() {
30                return glob_match_recursive(pattern, text, pi, ti + 1);
31            }
32            false
33        }
34        c => {
35            // Literal character - must match exactly
36            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
45/// Find the shortest prefix of text that matches the pattern
46fn 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
60/// Find the longest prefix of text that matches the pattern
61fn 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
76/// Find the shortest suffix of text that matches the pattern
77fn 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
91/// Find the longest suffix of text that matches the pattern
92fn 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/// Represents different types of parameter expansion modifiers
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum ParameterModifier {
110    /// No modifier - just ${VAR}
111    None,
112    /// ${VAR:-word} - use default if VAR is unset or null
113    Default(String),
114    /// ${VAR:=word} - assign default if VAR is unset or null
115    AssignDefault(String),
116    /// ${VAR:+word} - use alternative if VAR is set and not null
117    Alternative(String),
118    /// ${VAR:?word} - display error if VAR is unset or null
119    Error(String),
120    /// ${VAR:offset} - substring starting at offset
121    Substring(usize),
122    /// ${VAR:offset:length} - substring with length
123    SubstringWithLength(usize, usize),
124    /// ${VAR#pattern} - remove shortest match from beginning
125    RemoveShortestPrefix(String),
126    /// ${VAR##pattern} - remove longest match from beginning
127    RemoveLongestPrefix(String),
128    /// ${VAR%pattern} - remove shortest match from end
129    RemoveShortestSuffix(String),
130    /// ${VAR%%pattern} - remove longest match from end
131    RemoveLongestSuffix(String),
132    /// ${VAR/pattern/replacement} - substitute first match
133    Substitute(String, String),
134    /// ${VAR//pattern/replacement} - substitute all matches
135    SubstituteAll(String, String),
136    /// ${!name} - indirect expansion (value of variable named by name)
137    Indirect,
138    /// ${!prefix*} - names of variables starting with prefix
139    IndirectPrefix,
140    /// ${!prefix@} - names of variables starting with prefix (same as IndirectPrefix)
141    IndirectPrefixAt,
142}
143
144/// Represents a parameter expansion expression
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct ParameterExpansion {
147    pub var_name: String,
148    pub modifier: ParameterModifier,
149}
150
151/// Parse a parameter expansion from ${...} syntax
152pub 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    // Parse variable name
161    for ch in chars {
162        if ch == ':' || ch == '#' || ch == '%' || ch == '/' {
163            // Found a modifier - put back the character for modifier parsing
164            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            // Special case for indirect expansion ${!PREFIX*}
169            // The '!' is part of the variable name, continue parsing
170            var_name.push(ch);
171        } else if ch.is_alphanumeric() || ch == '_' || ch == '*' {
172            // Allow alphanumeric, underscore, and '*' (for indirect expansion)
173            var_name.push(ch);
174        } else {
175            return Err(format!("Invalid character '{}' in variable name", ch));
176        }
177    }
178
179    // No modifier found - check if this is an indirect expansion
180    let (final_var_name, modifier) = if let Some(stripped) = var_name.strip_prefix('!') {
181        if let Some(prefix_var) = stripped.strip_suffix('*') {
182            // Strip both the '!' prefix and '*' suffix from the var_name for IndirectPrefix
183            (
184                prefix_var.to_string(),
185                ParameterModifier::IndirectPrefix,
186            )
187        } else if let Some(prefix_var) = stripped.strip_suffix('@') {
188            // Strip both the '!' prefix and '@' suffix from the var_name for IndirectPrefixAt
189            (
190                prefix_var.to_string(),
191                ParameterModifier::IndirectPrefixAt,
192            )
193        } else {
194            // ${!name} - basic indirect expansion
195            (
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
210/// Parse a parameter modifier from the modifier string
211fn 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                    // ${VAR:=word}
223                    let word = modifier_str[2..].to_string();
224                    Ok(ParameterModifier::AssignDefault(word))
225                }
226                Some('-') => {
227                    // ${VAR:-word}
228                    let word = modifier_str[2..].to_string();
229                    Ok(ParameterModifier::Default(word))
230                }
231                Some('+') => {
232                    // ${VAR:+word}
233                    let word = modifier_str[2..].to_string();
234                    Ok(ParameterModifier::Alternative(word))
235                }
236                Some('?') => {
237                    // ${VAR:?word}
238                    let word = modifier_str[2..].to_string();
239                    Ok(ParameterModifier::Error(word))
240                }
241                Some(ch) if ch.is_ascii_digit() => {
242                    // ${VAR:offset} or ${VAR:offset:length}
243                    // Parse the substring syntax by analyzing the full modifier string
244
245                    // Extract the offset part (digits after the initial ':')
246                    let after_colon = &modifier_str[1..]; // Skip the initial ':'
247                    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                    // Check if there's a length specification
257                    if offset_end < after_colon.len() {
258                        // There's more content after the offset
259                        let after_offset = &after_colon[offset_end + 1..]; // Skip the ':' after offset
260                        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                // ${VAR##pattern}
279                Ok(ParameterModifier::RemoveLongestPrefix(pattern.to_string()))
280            } else if let Some(pattern) = modifier_str.strip_prefix('#') {
281                // ${VAR#pattern} - treat everything after # as pattern
282                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                // ${VAR%%pattern}
290                Ok(ParameterModifier::RemoveLongestSuffix(pattern.to_string()))
291            } else if let Some(pattern) = modifier_str.strip_prefix('%') {
292                // ${VAR%pattern}
293                Ok(ParameterModifier::RemoveShortestSuffix(pattern.to_string()))
294            } else {
295                Err(format!("Invalid suffix removal modifier: {}", modifier_str))
296            }
297        }
298        '/' => {
299            // Pattern substitution: ${VAR/pattern/replacement} or ${VAR//pattern/replacement}
300            let remaining: String = chars.as_str().to_string();
301
302            if modifier_str.starts_with("//") {
303                // Substitute all - skip the first '/' and find the pattern/replacement separator
304                let after_double_slash = &remaining[1..]; // Skip the first '/'
305                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                // Regular substitution
314                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
337/// Collect all variable names that start with the given prefix from all scopes
338fn collect_variable_names_with_prefix(prefix: &str, shell_state: &ShellState) -> Vec<String> {
339    let mut matching_vars = std::collections::HashSet::new();
340
341    // Collect from global variables
342    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    // Collect from local variable scopes
349    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    // Convert to sorted vector for consistent output
358    let mut result: Vec<String> = matching_vars.into_iter().collect();
359    result.sort();
360    result
361}
362
363/// Expand a parameter expression using the given shell state
364pub fn expand_parameter(
365    expansion: &ParameterExpansion,
366    shell_state: &ShellState,
367) -> Result<String, String> {
368    let value = match expansion.modifier {
369        ParameterModifier::None => {
370            // Simple variable expansion
371            shell_state.get_var(&expansion.var_name)
372        }
373        ParameterModifier::Indirect => {
374            // ${!name} - indirect expansion
375            // Get the value of the variable named by expansion.var_name
376            // Then use that value as a variable name to get the final value
377            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            // ${VAR:-word} - use default if VAR is unset or null
385            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            // ${VAR:=word} - assign default if VAR is unset or null
392            match shell_state.get_var(&expansion.var_name) {
393                Some(val) if !val.is_empty() => Some(val),
394                _ => {
395                    // Assign the default value
396                    Some(default.clone())
397                }
398            }
399        }
400        ParameterModifier::Alternative(ref alternative) => {
401            // ${VAR:+word} - use alternative if VAR is set and not null
402            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            // ${VAR:?word} - display error if VAR is unset or null
409            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            // ${VAR:offset}
423            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            // ${VAR:offset:length}
432            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            // ${VAR#pattern}
442            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            // ${VAR##pattern}
454            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            // ${VAR%pattern}
466            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            // ${VAR%%pattern}
478            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            // ${VAR/pattern/replacement}
490            if let Some(val) = shell_state.get_var(&expansion.var_name) {
491                // Simple string-based substitution for now
492                Some(val.replace(pattern, replacement))
493            } else {
494                Some("".to_string())
495            }
496        }
497        ParameterModifier::SubstituteAll(ref pattern, ref replacement) => {
498            // ${VAR//pattern/replacement}
499            if let Some(val) = shell_state.get_var(&expansion.var_name) {
500                // Simple string-based substitution for now
501                Some(val.replace(pattern, replacement))
502            } else {
503                Some("".to_string())
504            }
505        }
506        ParameterModifier::IndirectPrefix | ParameterModifier::IndirectPrefixAt => {
507            // ${!prefix*} - names of variables starting with prefix
508            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        // Should return variable names starting with "MY_" in sorted order
733        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        // Set global variables
741        shell_state.set_var("GLOBAL_VAR", "global".to_string());
742        shell_state.set_var("TEST_VAR1", "test1".to_string());
743
744        // Push local scope and set local variables
745        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        // Should find both global and local variables starting with "TEST_"
756        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        // Should return empty string when no variables match
772        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        // Empty prefix should match all variables
789        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        // Should work the same as IndirectPrefix for now
805        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        // Set global variables
813        shell_state.set_var("APP_CONFIG", "global_config".to_string());
814        shell_state.set_var("APP_DEBUG", "false".to_string());
815
816        // Push first local scope
817        shell_state.push_local_scope();
818        shell_state.set_local_var("APP_TEMP", "temp_value".to_string());
819
820        // Push second local scope
821        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        // Should find variables from all scopes
831        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        // Should find all variables starting with "TEST"
848        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        // Should resolve VAR_NAME -> "TARGET_VAR" -> "final_value"
871        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        // VAR_NAME is not set
879
880        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        // Should return empty string when intermediate variable is unset
887        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        // NONEXISTENT is not set
895
896        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        // Should return empty string when target variable is unset
903        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        // Set global variables
911        shell_state.set_var("VAR_NAME", "GLOBAL_TARGET".to_string());
912        shell_state.set_var("GLOBAL_TARGET", "global_value".to_string());
913        
914        // Push local scope and override
915        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        // Should use local scope value
926        assert_eq!(result, "local_value");
927    }
928}