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            (prefix_var.to_string(), ParameterModifier::IndirectPrefix)
184        } else if let Some(prefix_var) = stripped.strip_suffix('@') {
185            // Strip both the '!' prefix and '@' suffix from the var_name for IndirectPrefixAt
186            (prefix_var.to_string(), ParameterModifier::IndirectPrefixAt)
187        } else {
188            // ${!name} - basic indirect expansion
189            (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
201/// Parse a parameter modifier from the modifier string
202fn 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                    // ${VAR:=word}
214                    let word = modifier_str[2..].to_string();
215                    Ok(ParameterModifier::AssignDefault(word))
216                }
217                Some('-') => {
218                    // ${VAR:-word}
219                    let word = modifier_str[2..].to_string();
220                    Ok(ParameterModifier::Default(word))
221                }
222                Some('+') => {
223                    // ${VAR:+word}
224                    let word = modifier_str[2..].to_string();
225                    Ok(ParameterModifier::Alternative(word))
226                }
227                Some('?') => {
228                    // ${VAR:?word}
229                    let word = modifier_str[2..].to_string();
230                    Ok(ParameterModifier::Error(word))
231                }
232                Some(ch) if ch.is_ascii_digit() => {
233                    // ${VAR:offset} or ${VAR:offset:length}
234                    // Parse the substring syntax by analyzing the full modifier string
235
236                    // Extract the offset part (digits after the initial ':')
237                    let after_colon = &modifier_str[1..]; // Skip the initial ':'
238                    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                    // Check if there's a length specification
248                    if offset_end < after_colon.len() {
249                        // There's more content after the offset
250                        let after_offset = &after_colon[offset_end + 1..]; // Skip the ':' after offset
251                        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                // ${VAR##pattern}
270                Ok(ParameterModifier::RemoveLongestPrefix(pattern.to_string()))
271            } else if let Some(pattern) = modifier_str.strip_prefix('#') {
272                // ${VAR#pattern} - treat everything after # as pattern
273                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                // ${VAR%%pattern}
281                Ok(ParameterModifier::RemoveLongestSuffix(pattern.to_string()))
282            } else if let Some(pattern) = modifier_str.strip_prefix('%') {
283                // ${VAR%pattern}
284                Ok(ParameterModifier::RemoveShortestSuffix(pattern.to_string()))
285            } else {
286                Err(format!("Invalid suffix removal modifier: {}", modifier_str))
287            }
288        }
289        '/' => {
290            // Pattern substitution: ${VAR/pattern/replacement} or ${VAR//pattern/replacement}
291            let remaining: String = chars.as_str().to_string();
292
293            if modifier_str.starts_with("//") {
294                // Substitute all - skip the first '/' and find the pattern/replacement separator
295                let after_double_slash = &remaining[1..]; // Skip the first '/'
296                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                // Regular substitution
305                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
328/// Collect all variable names that start with the given prefix from all scopes
329fn collect_variable_names_with_prefix(prefix: &str, shell_state: &ShellState) -> Vec<String> {
330    let mut matching_vars = std::collections::HashSet::new();
331
332    // Collect from global variables
333    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    // Collect from local variable scopes
340    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    // Convert to sorted vector for consistent output
349    let mut result: Vec<String> = matching_vars.into_iter().collect();
350    result.sort();
351    result
352}
353
354/// Expand a parameter expression according to the shell state and parameter modifier.
355///
356/// On success returns the resulting expansion string. On error returns a diagnostic message
357/// (e.g., when nounset is enabled and an unset variable is expanded or when `${var:?msg}` fails).
358///
359/// # Examples
360///
361/// ```
362/// use rush_sh::parameter_expansion::{ParameterExpansion, ParameterModifier, expand_parameter};
363/// use rush_sh::ShellState;
364///
365/// let exp = ParameterExpansion { var_name: "VAR".to_string(), modifier: ParameterModifier::None };
366/// let mut state = ShellState::new();
367/// state.set_var("VAR", "value".to_string());
368/// let result = expand_parameter(&exp, &state).unwrap();
369/// assert_eq!(result, "value");
370/// ```
371pub fn expand_parameter(
372    expansion: &ParameterExpansion,
373    shell_state: &ShellState,
374) -> Result<String, String> {
375    let value = match expansion.modifier {
376        ParameterModifier::None => {
377            // Simple variable expansion
378            let var_value = shell_state.get_var(&expansion.var_name);
379            
380            // Check nounset option (-u): Treat unset variables as an error
381            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            // ${!name} - indirect expansion
389            // Get the value of the variable named by expansion.var_name
390            // Then use that value as a variable name to get the final value
391            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            // ${VAR:-word} - use default if VAR is unset or null
399            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            // ${VAR:=word} - assign default if VAR is unset or null
406            match shell_state.get_var(&expansion.var_name) {
407                Some(val) if !val.is_empty() => Some(val),
408                _ => {
409                    // Assign the default value
410                    Some(default.clone())
411                }
412            }
413        }
414        ParameterModifier::Alternative(ref alternative) => {
415            // ${VAR:+word} - use alternative if VAR is set and not null
416            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            // ${VAR:?word} - display error if VAR is unset or null
423            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            // ${VAR:offset}
437            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            // ${VAR:offset:length}
446            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            // ${VAR#pattern}
456            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            // ${VAR##pattern}
468            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            // ${VAR%pattern}
480            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            // ${VAR%%pattern}
492            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            // ${VAR/pattern/replacement}
504            if let Some(val) = shell_state.get_var(&expansion.var_name) {
505                // Simple string-based substitution for now
506                Some(val.replace(pattern, replacement))
507            } else {
508                Some("".to_string())
509            }
510        }
511        ParameterModifier::SubstituteAll(ref pattern, ref replacement) => {
512            // ${VAR//pattern/replacement}
513            if let Some(val) = shell_state.get_var(&expansion.var_name) {
514                // Simple string-based substitution for now
515                Some(val.replace(pattern, replacement))
516            } else {
517                Some("".to_string())
518            }
519        }
520        ParameterModifier::IndirectPrefix | ParameterModifier::IndirectPrefixAt => {
521            // ${!prefix*} - names of variables starting with prefix
522            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        // Should return variable names starting with "MY_" in sorted order
748        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        // Set global variables
756        shell_state.set_var("GLOBAL_VAR", "global".to_string());
757        shell_state.set_var("TEST_VAR1", "test1".to_string());
758
759        // Push local scope and set local variables
760        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        // Should find both global and local variables starting with "TEST_"
771        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        // Should return empty string when no variables match
787        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        // Empty prefix should match all variables
804        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        // Should work the same as IndirectPrefix for now
820        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        // Set global variables
828        shell_state.set_var("APP_CONFIG", "global_config".to_string());
829        shell_state.set_var("APP_DEBUG", "false".to_string());
830
831        // Push first local scope
832        shell_state.push_local_scope();
833        shell_state.set_local_var("APP_TEMP", "temp_value".to_string());
834
835        // Push second local scope
836        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        // Should find variables from all scopes
846        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        // Should find all variables starting with "TEST"
863        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        // Should resolve VAR_NAME -> "TARGET_VAR" -> "final_value"
886        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        // VAR_NAME is not set
894
895        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        // Should return empty string when intermediate variable is unset
902        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        // NONEXISTENT is not set
910
911        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        // Should return empty string when target variable is unset
918        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        // Set global variables
926        shell_state.set_var("VAR_NAME", "GLOBAL_TARGET".to_string());
927        shell_state.set_var("GLOBAL_TARGET", "global_value".to_string());
928
929        // Push local scope and override
930        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        // Should use local scope value
941        assert_eq!(result, "local_value");
942    }
943}