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    /// ${!prefix*} - names of variables starting with prefix
137    IndirectPrefix,
138    /// ${!prefix@} - names of variables starting with prefix (same as IndirectPrefix)
139    IndirectPrefixAt,
140}
141
142/// Represents a parameter expansion expression
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct ParameterExpansion {
145    pub var_name: String,
146    pub modifier: ParameterModifier,
147}
148
149/// Parse a parameter expansion from ${...} syntax
150pub fn parse_parameter_expansion(content: &str) -> Result<ParameterExpansion, String> {
151    if content.is_empty() {
152        return Err("Empty parameter expansion".to_string());
153    }
154
155    let chars = content.chars();
156    let mut var_name = String::new();
157
158    // Parse variable name
159    for ch in chars {
160        if ch == ':' || ch == '#' || ch == '%' || ch == '/' {
161            // Found a modifier - put back the character for modifier parsing
162            let modifier_str: String = content[var_name.len()..].to_string();
163            let modifier = parse_modifier(&modifier_str)?;
164            return Ok(ParameterExpansion { var_name, modifier });
165        } else if ch == '!' {
166            // Special case for indirect expansion ${!PREFIX*}
167            // The '!' is part of the variable name, continue parsing
168            var_name.push(ch);
169        } else if ch.is_alphanumeric() || ch == '_' || ch == '*' {
170            // Allow alphanumeric, underscore, and '*' (for indirect expansion)
171            var_name.push(ch);
172        } else {
173            return Err(format!("Invalid character '{}' in variable name", ch));
174        }
175    }
176
177    // No modifier found - check if this is an indirect expansion
178    let (final_var_name, modifier) = if var_name.starts_with('!') {
179        if var_name.ends_with('*') {
180            // Strip the '*' from the var_name for IndirectPrefix
181            (
182                var_name[..var_name.len() - 1].to_string(),
183                ParameterModifier::IndirectPrefix,
184            )
185        } else if var_name.ends_with('@') {
186            // Strip the '@' from the var_name for IndirectPrefixAt
187            (
188                var_name[..var_name.len() - 1].to_string(),
189                ParameterModifier::IndirectPrefixAt,
190            )
191        } else {
192            return Err("Invalid indirect expansion: must end with * or @".to_string());
193        }
194    } else {
195        (var_name, ParameterModifier::None)
196    };
197
198    Ok(ParameterExpansion {
199        var_name: final_var_name,
200        modifier,
201    })
202}
203
204/// Parse a parameter modifier from the modifier string
205fn parse_modifier(modifier_str: &str) -> Result<ParameterModifier, String> {
206    if modifier_str.is_empty() {
207        return Ok(ParameterModifier::None);
208    }
209
210    let mut chars = modifier_str.chars();
211
212    match chars.next().unwrap() {
213        ':' => {
214            match chars.next() {
215                Some('=') => {
216                    // ${VAR:=word}
217                    let word = modifier_str[2..].to_string();
218                    Ok(ParameterModifier::AssignDefault(word))
219                }
220                Some('-') => {
221                    // ${VAR:-word}
222                    let word = modifier_str[2..].to_string();
223                    Ok(ParameterModifier::Default(word))
224                }
225                Some('+') => {
226                    // ${VAR:+word}
227                    let word = modifier_str[2..].to_string();
228                    Ok(ParameterModifier::Alternative(word))
229                }
230                Some('?') => {
231                    // ${VAR:?word}
232                    let word = modifier_str[2..].to_string();
233                    Ok(ParameterModifier::Error(word))
234                }
235                Some(ch) if ch.is_ascii_digit() => {
236                    // ${VAR:offset} or ${VAR:offset:length}
237                    // Parse the substring syntax by analyzing the full modifier string
238
239                    // Extract the offset part (digits after the initial ':')
240                    let after_colon = &modifier_str[1..]; // Skip the initial ':'
241                    let offset_end = after_colon.find(':').unwrap_or(after_colon.len());
242                    let offset_str = &after_colon[..offset_end];
243
244                    if offset_str.is_empty() {
245                        return Err("Missing offset in substring operation".to_string());
246                    }
247
248                    let offset: usize = offset_str.parse().map_err(|_| "Invalid offset number")?;
249
250                    // Check if there's a length specification
251                    if offset_end < after_colon.len() {
252                        // There's more content after the offset
253                        let after_offset = &after_colon[offset_end + 1..]; // Skip the ':' after offset
254                        if !after_offset.is_empty()
255                            && after_offset.chars().all(|c| c.is_ascii_digit())
256                        {
257                            let length: usize =
258                                after_offset.parse().map_err(|_| "Invalid length number")?;
259                            Ok(ParameterModifier::SubstringWithLength(offset, length))
260                        } else {
261                            Ok(ParameterModifier::Substring(offset))
262                        }
263                    } else {
264                        Ok(ParameterModifier::Substring(offset))
265                    }
266                }
267                _ => Err(format!("Invalid modifier: {}", modifier_str)),
268            }
269        }
270        '#' => {
271            if let Some(pattern) = modifier_str.strip_prefix("##") {
272                // ${VAR##pattern}
273                Ok(ParameterModifier::RemoveLongestPrefix(pattern.to_string()))
274            } else if let Some(pattern) = modifier_str.strip_prefix('#') {
275                // ${VAR#pattern} - treat everything after # as pattern
276                Ok(ParameterModifier::RemoveShortestPrefix(pattern.to_string()))
277            } else {
278                Err(format!("Invalid prefix removal modifier: {}", modifier_str))
279            }
280        }
281        '%' => {
282            if let Some(pattern) = modifier_str.strip_prefix("%%") {
283                // ${VAR%%pattern}
284                Ok(ParameterModifier::RemoveLongestSuffix(pattern.to_string()))
285            } else if let Some(pattern) = modifier_str.strip_prefix('%') {
286                // ${VAR%pattern}
287                Ok(ParameterModifier::RemoveShortestSuffix(pattern.to_string()))
288            } else {
289                Err(format!("Invalid suffix removal modifier: {}", modifier_str))
290            }
291        }
292        '/' => {
293            // Pattern substitution: ${VAR/pattern/replacement} or ${VAR//pattern/replacement}
294            let remaining: String = chars.as_str().to_string();
295
296            if modifier_str.starts_with("//") {
297                // Substitute all - skip the first '/' and find the pattern/replacement separator
298                let after_double_slash = &remaining[1..]; // Skip the first '/'
299                if let Some(slash_pos) = after_double_slash.find('/') {
300                    let pattern = after_double_slash[..slash_pos].to_string();
301                    let replacement = after_double_slash[slash_pos + 1..].to_string();
302                    Ok(ParameterModifier::SubstituteAll(pattern, replacement))
303                } else {
304                    Err("Invalid substitution syntax: missing replacement".to_string())
305                }
306            } else {
307                // Regular substitution
308                if let Some(slash_pos) = remaining.find('/') {
309                    let pattern = remaining[..slash_pos].to_string();
310                    let replacement = remaining[slash_pos + 1..].to_string();
311                    Ok(ParameterModifier::Substitute(pattern, replacement))
312                } else {
313                    Err("Invalid substitution syntax: missing replacement".to_string())
314                }
315            }
316        }
317        '!' => {
318            let prefix = modifier_str[1..].to_string();
319            if prefix.ends_with('*') {
320                Ok(ParameterModifier::IndirectPrefix)
321            } else if prefix.ends_with('@') {
322                Ok(ParameterModifier::IndirectPrefixAt)
323            } else {
324                Err("Invalid indirect expansion: must end with * or @".to_string())
325            }
326        }
327        _ => Err(format!("Unknown modifier: {}", modifier_str)),
328    }
329}
330
331/// Expand a parameter expression using the given shell state
332pub fn expand_parameter(
333    expansion: &ParameterExpansion,
334    shell_state: &ShellState,
335) -> Result<String, String> {
336    let value = match expansion.modifier {
337        ParameterModifier::None => {
338            // Simple variable expansion
339            shell_state.get_var(&expansion.var_name)
340        }
341        ParameterModifier::Default(ref default) => {
342            // ${VAR:-word} - use default if VAR is unset or null
343            match shell_state.get_var(&expansion.var_name) {
344                Some(val) if !val.is_empty() => Some(val),
345                _ => Some(default.clone()),
346            }
347        }
348        ParameterModifier::AssignDefault(ref default) => {
349            // ${VAR:=word} - assign default if VAR is unset or null
350            match shell_state.get_var(&expansion.var_name) {
351                Some(val) if !val.is_empty() => Some(val),
352                _ => {
353                    // Assign the default value
354                    Some(default.clone())
355                }
356            }
357        }
358        ParameterModifier::Alternative(ref alternative) => {
359            // ${VAR:+word} - use alternative if VAR is set and not null
360            match shell_state.get_var(&expansion.var_name) {
361                Some(val) if !val.is_empty() => Some(alternative.clone()),
362                _ => Some("".to_string()),
363            }
364        }
365        ParameterModifier::Error(ref error_msg) => {
366            // ${VAR:?word} - display error if VAR is unset or null
367            match shell_state.get_var(&expansion.var_name) {
368                Some(val) if !val.is_empty() => Some(val),
369                _ => {
370                    let msg = if error_msg.is_empty() {
371                        format!("parameter '{}' not set", expansion.var_name)
372                    } else {
373                        error_msg.clone()
374                    };
375                    return Err(msg);
376                }
377            }
378        }
379        ParameterModifier::Substring(offset) => {
380            // ${VAR:offset}
381            if let Some(val) = shell_state.get_var(&expansion.var_name) {
382                let start = offset.min(val.len());
383                Some(val[start..].to_string())
384            } else {
385                Some("".to_string())
386            }
387        }
388        ParameterModifier::SubstringWithLength(offset, length) => {
389            // ${VAR:offset:length}
390            if let Some(val) = shell_state.get_var(&expansion.var_name) {
391                let start = offset.min(val.len());
392                let end = (start + length).min(val.len());
393                Some(val[start..end].to_string())
394            } else {
395                Some("".to_string())
396            }
397        }
398        ParameterModifier::RemoveShortestPrefix(ref pattern) => {
399            // ${VAR#pattern}
400            if let Some(val) = shell_state.get_var(&expansion.var_name) {
401                if let Some(match_end) = find_shortest_prefix_match(pattern, &val) {
402                    Some(val[match_end..].to_string())
403                } else {
404                    Some(val.clone())
405                }
406            } else {
407                Some("".to_string())
408            }
409        }
410        ParameterModifier::RemoveLongestPrefix(ref pattern) => {
411            // ${VAR##pattern}
412            if let Some(val) = shell_state.get_var(&expansion.var_name) {
413                if let Some(match_end) = find_longest_prefix_match(pattern, &val) {
414                    Some(val[match_end..].to_string())
415                } else {
416                    Some(val.clone())
417                }
418            } else {
419                Some("".to_string())
420            }
421        }
422        ParameterModifier::RemoveShortestSuffix(ref pattern) => {
423            // ${VAR%pattern}
424            if let Some(val) = shell_state.get_var(&expansion.var_name) {
425                if let Some(match_start) = find_shortest_suffix_match(pattern, &val) {
426                    Some(val[..match_start].to_string())
427                } else {
428                    Some(val.clone())
429                }
430            } else {
431                Some("".to_string())
432            }
433        }
434        ParameterModifier::RemoveLongestSuffix(ref pattern) => {
435            // ${VAR%%pattern}
436            if let Some(val) = shell_state.get_var(&expansion.var_name) {
437                if let Some(match_start) = find_longest_suffix_match(pattern, &val) {
438                    Some(val[..match_start].to_string())
439                } else {
440                    Some(val.clone())
441                }
442            } else {
443                Some("".to_string())
444            }
445        }
446        ParameterModifier::Substitute(ref pattern, ref replacement) => {
447            // ${VAR/pattern/replacement}
448            if let Some(val) = shell_state.get_var(&expansion.var_name) {
449                // Simple string-based substitution for now
450                Some(val.replace(pattern, replacement))
451            } else {
452                Some("".to_string())
453            }
454        }
455        ParameterModifier::SubstituteAll(ref pattern, ref replacement) => {
456            // ${VAR//pattern/replacement}
457            if let Some(val) = shell_state.get_var(&expansion.var_name) {
458                // Simple string-based substitution for now
459                Some(val.replace(pattern, replacement))
460            } else {
461                Some("".to_string())
462            }
463        }
464        ParameterModifier::IndirectPrefix | ParameterModifier::IndirectPrefixAt => {
465            // ${!prefix*}
466            // For now, return empty string as this is complex to implement
467            // TODO: Implement indirect expansion properly
468            Some("".to_string())
469        }
470    };
471
472    Ok(value.unwrap_or_else(|| "".to_string()))
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    #[test]
480    fn test_parse_simple_variable() {
481        let result = parse_parameter_expansion("VAR").unwrap();
482        assert_eq!(result.var_name, "VAR");
483        assert_eq!(result.modifier, ParameterModifier::None);
484    }
485
486    #[test]
487    fn test_parse_default_modifier() {
488        let result = parse_parameter_expansion("VAR:-default").unwrap();
489        assert_eq!(result.var_name, "VAR");
490        assert_eq!(
491            result.modifier,
492            ParameterModifier::Default("default".to_string())
493        );
494    }
495
496    #[test]
497    fn test_parse_assign_default_modifier() {
498        let result = parse_parameter_expansion("VAR:=default").unwrap();
499        assert_eq!(result.var_name, "VAR");
500        assert_eq!(
501            result.modifier,
502            ParameterModifier::AssignDefault("default".to_string())
503        );
504    }
505
506    #[test]
507    fn test_parse_alternative_modifier() {
508        let result = parse_parameter_expansion("VAR:+alt").unwrap();
509        assert_eq!(result.var_name, "VAR");
510        assert_eq!(
511            result.modifier,
512            ParameterModifier::Alternative("alt".to_string())
513        );
514    }
515
516    #[test]
517    fn test_parse_error_modifier() {
518        let result = parse_parameter_expansion("VAR:?error").unwrap();
519        assert_eq!(result.var_name, "VAR");
520        assert_eq!(
521            result.modifier,
522            ParameterModifier::Error("error".to_string())
523        );
524    }
525
526    #[test]
527    fn test_parse_substring() {
528        let result = parse_parameter_expansion("VAR:5").unwrap();
529        assert_eq!(result.var_name, "VAR");
530        assert_eq!(result.modifier, ParameterModifier::Substring(5));
531    }
532
533    #[test]
534    fn test_parse_substring_with_length() {
535        let result = parse_parameter_expansion("VAR:2:3").unwrap();
536        assert_eq!(result.var_name, "VAR");
537        assert_eq!(
538            result.modifier,
539            ParameterModifier::SubstringWithLength(2, 3)
540        );
541    }
542
543    #[test]
544    fn test_parse_remove_shortest_prefix() {
545        let result = parse_parameter_expansion("VAR#prefix").unwrap();
546        assert_eq!(result.var_name, "VAR");
547        assert_eq!(
548            result.modifier,
549            ParameterModifier::RemoveShortestPrefix("prefix".to_string())
550        );
551    }
552
553    #[test]
554    fn test_parse_remove_longest_prefix() {
555        let result = parse_parameter_expansion("VAR##prefix").unwrap();
556        assert_eq!(result.var_name, "VAR");
557        assert_eq!(
558            result.modifier,
559            ParameterModifier::RemoveLongestPrefix("prefix".to_string())
560        );
561    }
562
563    #[test]
564    fn test_parse_remove_shortest_suffix() {
565        let result = parse_parameter_expansion("VAR%suffix").unwrap();
566        assert_eq!(result.var_name, "VAR");
567        assert_eq!(
568            result.modifier,
569            ParameterModifier::RemoveShortestSuffix("suffix".to_string())
570        );
571    }
572
573    #[test]
574    fn test_parse_remove_longest_suffix() {
575        let result = parse_parameter_expansion("VAR%%suffix").unwrap();
576        assert_eq!(result.var_name, "VAR");
577        assert_eq!(
578            result.modifier,
579            ParameterModifier::RemoveLongestSuffix("suffix".to_string())
580        );
581    }
582
583    #[test]
584    fn test_parse_substitute() {
585        let result = parse_parameter_expansion("VAR/old/new").unwrap();
586        assert_eq!(result.var_name, "VAR");
587        assert_eq!(
588            result.modifier,
589            ParameterModifier::Substitute("old".to_string(), "new".to_string())
590        );
591    }
592
593    #[test]
594    fn test_parse_substitute_all() {
595        let result = parse_parameter_expansion("VAR//old/new").unwrap();
596        assert_eq!(result.var_name, "VAR");
597        assert_eq!(
598            result.modifier,
599            ParameterModifier::SubstituteAll("old".to_string(), "new".to_string())
600        );
601    }
602
603    #[test]
604    fn test_parse_indirect_prefix() {
605        let result = parse_parameter_expansion("!PREFIX*").unwrap();
606        assert_eq!(result.var_name, "!PREFIX");
607        assert_eq!(result.modifier, ParameterModifier::IndirectPrefix);
608    }
609
610    #[test]
611    fn test_parse_empty() {
612        let result = parse_parameter_expansion("");
613        assert!(result.is_err());
614    }
615
616    #[test]
617    fn test_parse_invalid_character() {
618        let result = parse_parameter_expansion("VAR@test");
619        assert!(result.is_err());
620    }
621
622    #[test]
623    fn test_expand_simple_variable() {
624        let mut shell_state = ShellState::new();
625        shell_state.set_var("TEST_VAR", "hello world".to_string());
626
627        let expansion = ParameterExpansion {
628            var_name: "TEST_VAR".to_string(),
629            modifier: ParameterModifier::None,
630        };
631
632        let result = expand_parameter(&expansion, &shell_state).unwrap();
633        assert_eq!(result, "hello world");
634    }
635
636    #[test]
637    fn test_expand_default_modifier() {
638        let mut shell_state = ShellState::new();
639        shell_state.set_var("TEST_VAR", "value".to_string());
640
641        let expansion = ParameterExpansion {
642            var_name: "TEST_VAR".to_string(),
643            modifier: ParameterModifier::Default("default".to_string()),
644        };
645
646        let result = expand_parameter(&expansion, &shell_state).unwrap();
647        assert_eq!(result, "value");
648    }
649
650    #[test]
651    fn test_expand_default_modifier_unset() {
652        let shell_state = ShellState::new();
653
654        let expansion = ParameterExpansion {
655            var_name: "UNSET_VAR".to_string(),
656            modifier: ParameterModifier::Default("default".to_string()),
657        };
658
659        let result = expand_parameter(&expansion, &shell_state).unwrap();
660        assert_eq!(result, "default");
661    }
662
663    #[test]
664    fn test_expand_substring() {
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::Substring(6),
671        };
672
673        let result = expand_parameter(&expansion, &shell_state).unwrap();
674        assert_eq!(result, "world");
675    }
676}