rush_sh/executor/
expansion.rs

1//! Variable and wildcard expansion functionality for the Rush shell.
2//!
3//! This module handles the expansion of shell variables, command substitutions,
4//! arithmetic expressions, and wildcard patterns in command arguments.
5
6use crate::parser::Ast;
7use crate::state::ShellState;
8
9/// Expand variables in a list of argument strings.
10///
11/// Processes each argument through [`expand_variables_in_string`] to perform
12/// variable expansion, command substitution, and arithmetic evaluation.
13///
14/// # Arguments
15/// * `args` - Slice of argument strings to expand
16/// * `shell_state` - Mutable reference to shell state for variable lookups
17///
18/// # Returns
19/// Vector of expanded argument strings
20pub(crate) fn expand_variables_in_args(args: &[String], shell_state: &mut ShellState) -> Vec<String> {
21    let mut expanded_args = Vec::new();
22
23    for arg in args {
24        // Expand variables within the argument string
25        let expanded_arg = expand_variables_in_string(arg, shell_state);
26        expanded_args.push(expanded_arg);
27    }
28
29    expanded_args
30}
31
32/// Expands shell-style variables, command substitutions, arithmetic expressions, and backtick substitutions inside a string.
33///
34/// This function processes `$VAR` and positional/special parameters (`$1`, `$?`, `$#`, `$*`, `$@`, `$$`, `$0`), command substitutions using `$(...)` and backticks, and arithmetic expansions using `$((...))`, producing the resulting string with substitutions applied. Undefined numeric positional parameters and the documented special parameters expand to an empty string; other undefined variable names are left as literal `$NAME`. Arithmetic evaluation errors are rendered as an error message (colorized when the shell state enables colors). Command substitutions are parsed and executed using the current shell state; on failure the original substitution text is preserved.
35///
36/// # Examples
37///
38/// ```no_run
39/// use rush_sh::ShellState;
40/// use rush_sh::executor::expand_variables_in_string;
41/// // assume `shell_state` is a mutable ShellState with VAR=hello
42/// let mut shell_state = ShellState::new();
43/// shell_state.set_var("VAR", "hello".to_string());
44/// let input = "Value:$VAR";
45/// let out = expand_variables_in_string(input, &mut shell_state);
46/// assert_eq!(out, "Value:hello");
47/// ```
48///
49/// # Errors
50///
51/// Returns `Err` if input cannot be tokenized
52pub fn expand_variables_in_string(input: &str, shell_state: &mut ShellState) -> String {
53    let mut result = String::new();
54    let mut chars = input.chars().peekable();
55
56    while let Some(ch) = chars.next() {
57        if ch == '$' {
58            // Check for command substitution $(...) or arithmetic expansion $((...))
59            if let Some(&'(') = chars.peek() {
60                chars.next(); // consume first (
61
62                // Check if this is arithmetic expansion $((...))
63                if let Some(&'(') = chars.peek() {
64                    // Arithmetic expansion $((...))
65                    chars.next(); // consume second (
66                    let mut arithmetic_expr = String::new();
67                    let mut paren_depth = 1;
68                    let mut found_closing = false;
69
70                    while let Some(c) = chars.next() {
71                        if c == '(' {
72                            paren_depth += 1;
73                            arithmetic_expr.push(c);
74                        } else if c == ')' {
75                            paren_depth -= 1;
76                            if paren_depth == 0 {
77                                // Found the first closing ) - check for second )
78                                if let Some(&')') = chars.peek() {
79                                    chars.next(); // consume the second )
80                                    found_closing = true;
81                                    break;
82                                } else {
83                                    // Missing second closing paren, treat as error
84                                    result.push_str("$((");
85                                    result.push_str(&arithmetic_expr);
86                                    result.push(')');
87                                    break;
88                                }
89                            }
90                            arithmetic_expr.push(c);
91                        } else {
92                            arithmetic_expr.push(c);
93                        }
94                    }
95
96                    if found_closing {
97                        // First expand variables in the arithmetic expression
98                        // The arithmetic evaluator expects variable names without $ prefix
99                        // So we need to expand $VAR to the value before evaluation
100                        let mut expanded_expr = String::new();
101                        let mut expr_chars = arithmetic_expr.chars().peekable();
102
103                        while let Some(ch) = expr_chars.next() {
104                            if ch == '$' {
105                                // Expand variable
106                                let mut var_name = String::new();
107                                if let Some(&c) = expr_chars.peek() {
108                                    if c == '?'
109                                        || c == '$'
110                                        || c == '0'
111                                        || c == '#'
112                                        || c == '*'
113                                        || c == '@'
114                                        || c.is_ascii_digit()
115                                    {
116                                        var_name.push(c);
117                                        expr_chars.next();
118                                    } else {
119                                        while let Some(&c) = expr_chars.peek() {
120                                            if c.is_alphanumeric() || c == '_' {
121                                                var_name.push(c);
122                                                expr_chars.next();
123                                            } else {
124                                                break;
125                                            }
126                                        }
127                                    }
128                                }
129
130                                if !var_name.is_empty() {
131                                    if let Some(value) = shell_state.get_var(&var_name) {
132                                        expanded_expr.push_str(&value);
133                                    } else {
134                                        // Variable not found, use 0 for arithmetic
135                                        expanded_expr.push('0');
136                                    }
137                                } else {
138                                    expanded_expr.push('$');
139                                }
140                            } else {
141                                expanded_expr.push(ch);
142                            }
143                        }
144
145                        match crate::arithmetic::evaluate_arithmetic_expression(
146                            &expanded_expr,
147                            shell_state,
148                        ) {
149                            Ok(value) => {
150                                result.push_str(&value.to_string());
151                            }
152                            Err(e) => {
153                                // On arithmetic error, display a proper error message
154                                if shell_state.colors_enabled {
155                                    result.push_str(&format!(
156                                        "{}arithmetic error: {}{}",
157                                        shell_state.color_scheme.error, e, "\x1b[0m"
158                                    ));
159                                } else {
160                                    result.push_str(&format!("arithmetic error: {}", e));
161                                }
162                            }
163                        }
164                    } else {
165                        // Didn't find proper closing - keep as literal
166                        result.push_str("$((");
167                        result.push_str(&arithmetic_expr);
168                        // Note: we don't add closing parens since they weren't in the input
169                    }
170                    continue;
171                }
172
173                // Regular command substitution $(...)
174                let mut sub_command = String::new();
175                let mut paren_depth = 1;
176
177                for c in chars.by_ref() {
178                    if c == '(' {
179                        paren_depth += 1;
180                        sub_command.push(c);
181                    } else if c == ')' {
182                        paren_depth -= 1;
183                        if paren_depth == 0 {
184                            break;
185                        }
186                        sub_command.push(c);
187                    } else {
188                        sub_command.push(c);
189                    }
190                }
191
192                // Execute the command substitution within the current shell context
193                // Parse and execute the command using our own lexer/parser/executor
194                if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
195                    // Expand aliases before parsing
196                    let expanded_tokens = match crate::lexer::expand_aliases(
197                        tokens,
198                        shell_state,
199                        &mut std::collections::HashSet::new(),
200                    ) {
201                        Ok(t) => t,
202                        Err(_) => {
203                            // Alias expansion error, keep literal
204                            result.push_str("$(");
205                            result.push_str(&sub_command);
206                            result.push(')');
207                            continue;
208                        }
209                    };
210
211                    match crate::parser::parse(expanded_tokens) {
212                        Ok(ast) => {
213                            // Execute within current shell context and capture output
214                            match super::execute_and_capture_output(ast, shell_state) {
215                                Ok(output) => {
216                                    result.push_str(&output);
217                                }
218                                Err(_) => {
219                                    // On failure, keep the literal
220                                    result.push_str("$(");
221                                    result.push_str(&sub_command);
222                                    result.push(')');
223                                }
224                            }
225                        }
226                        Err(_parse_err) => {
227                            // Parse error - try to handle as function call if it looks like one
228                            let tokens_str = sub_command.trim();
229                            if tokens_str.contains(' ') {
230                                // Split by spaces and check if first token looks like a function call
231                                let parts: Vec<&str> = tokens_str.split_whitespace().collect();
232                                if let Some(first_token) = parts.first()
233                                    && shell_state.get_function(first_token).is_some()
234                                {
235                                    // This is a function call, create AST manually
236                                    let function_call = Ast::FunctionCall {
237                                        name: first_token.to_string(),
238                                        args: parts[1..].iter().map(|s| s.to_string()).collect(),
239                                    };
240                                    match super::execute_and_capture_output(function_call, shell_state) {
241                                        Ok(output) => {
242                                            result.push_str(&output);
243                                            continue;
244                                        }
245                                        Err(_) => {
246                                            // Fall back to literal
247                                        }
248                                    }
249                                }
250                            }
251                            // Keep the literal
252                            result.push_str("$(");
253                            result.push_str(&sub_command);
254                            result.push(')');
255                        }
256                    }
257                } else {
258                    // Lex error, keep literal
259                    result.push_str("$(");
260                    result.push_str(&sub_command);
261                    result.push(')');
262                }
263            } else {
264                // Regular variable
265                let mut var_name = String::new();
266                let mut next_ch = chars.peek();
267
268                // Handle special single-character variables first
269                if let Some(&c) = next_ch {
270                    if c == '?' || c == '$' || c == '0' || c == '#' || c == '*' || c == '@' {
271                        var_name.push(c);
272                        chars.next(); // consume the character
273                    } else if c.is_ascii_digit() {
274                        // Positional parameter
275                        var_name.push(c);
276                        chars.next();
277                    } else {
278                        // Regular variable name
279                        while let Some(&c) = next_ch {
280                            if c.is_alphanumeric() || c == '_' {
281                                var_name.push(c);
282                                chars.next(); // consume the character
283                                next_ch = chars.peek();
284                            } else {
285                                break;
286                            }
287                        }
288                    }
289                }
290
291                if !var_name.is_empty() {
292                    if let Some(value) = shell_state.get_var(&var_name) {
293                        result.push_str(&value);
294                    } else {
295                        // Variable not found - for positional parameters, expand to empty string
296                        // For other variables, keep the literal
297                        if var_name.chars().next().unwrap().is_ascii_digit()
298                            || var_name == "?"
299                            || var_name == "$"
300                            || var_name == "0"
301                            || var_name == "#"
302                            || var_name == "*"
303                            || var_name == "@"
304                        {
305                            // Expand to empty string for undefined positional parameters
306                        } else {
307                            // Keep the literal for regular variables
308                            result.push('$');
309                            result.push_str(&var_name);
310                        }
311                    }
312                } else {
313                    result.push('$');
314                }
315            }
316        } else if ch == '`' {
317            // Backtick command substitution
318            let mut sub_command = String::new();
319
320            for c in chars.by_ref() {
321                if c == '`' {
322                    break;
323                }
324                sub_command.push(c);
325            }
326
327            // Execute the command substitution
328            if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
329                // Expand aliases before parsing
330                let expanded_tokens = match crate::lexer::expand_aliases(
331                    tokens,
332                    shell_state,
333                    &mut std::collections::HashSet::new(),
334                ) {
335                    Ok(t) => t,
336                    Err(_) => {
337                        // Alias expansion error, keep literal
338                        result.push('`');
339                        result.push_str(&sub_command);
340                        result.push('`');
341                        continue;
342                    }
343                };
344
345                if let Ok(ast) = crate::parser::parse(expanded_tokens) {
346                    // Execute and capture output
347                    match super::execute_and_capture_output(ast, shell_state) {
348                        Ok(output) => {
349                            result.push_str(&output);
350                        }
351                        Err(_) => {
352                            // On failure, keep the literal
353                            result.push('`');
354                            result.push_str(&sub_command);
355                            result.push('`');
356                        }
357                    }
358                } else {
359                    // Parse error, keep literal
360                    result.push('`');
361                    result.push_str(&sub_command);
362                    result.push('`');
363                }
364            } else {
365                // Lex error, keep literal
366                result.push('`');
367                result.push_str(&sub_command);
368                result.push('`');
369            }
370        } else {
371            result.push(ch);
372        }
373    }
374
375    result
376}
377
378/// Expand shell-style wildcard patterns in a list of arguments unless the `noglob` option is set.
379///
380/// Patterns containing `*`, `?`, or `[` are replaced by the sorted list of matching filesystem paths. If a pattern has no matches or is an invalid pattern, the original literal argument is kept. If the shell state's `noglob` option is enabled, all arguments are returned unchanged.
381///
382/// # Examples
383///
384/// ```
385/// // Note: expand_wildcards is a private function
386/// // This example is for documentation only
387/// ```
388pub(crate) fn expand_wildcards(args: &[String], shell_state: &ShellState) -> Result<Vec<String>, String> {
389    let mut expanded_args = Vec::new();
390
391    for arg in args {
392        // Skip wildcard expansion if noglob option (-f) is enabled
393        if shell_state.options.noglob {
394            expanded_args.push(arg.clone());
395            continue;
396        }
397        
398        if arg.contains('*') || arg.contains('?') || arg.contains('[') {
399            // Try to expand wildcard
400            match glob::glob(arg) {
401                Ok(paths) => {
402                    let mut matches: Vec<String> = paths
403                        .filter_map(|p| p.ok())
404                        .map(|p| p.to_string_lossy().to_string())
405                        .collect();
406                    if matches.is_empty() {
407                        // No matches, keep literal
408                        expanded_args.push(arg.clone());
409                    } else {
410                        // Sort for consistent behavior
411                        matches.sort();
412                        expanded_args.extend(matches);
413                    }
414                }
415                Err(_e) => {
416                    // Invalid pattern, keep literal
417                    expanded_args.push(arg.clone());
418                }
419            }
420        } else {
421            expanded_args.push(arg.clone());
422        }
423    }
424    Ok(expanded_args)
425}