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 if let Some(&'{') = chars.peek() {
264                // ${VAR} syntax
265                chars.next(); // consume the {
266                let mut var_name = String::new();
267                let mut found_closing = false;
268
269                // Read until we find the closing }
270                while let Some(c) = chars.next() {
271                    if c == '}' {
272                        found_closing = true;
273                        break;
274                    }
275                    var_name.push(c);
276                }
277
278                if found_closing && !var_name.is_empty() {
279                    if let Some(value) = shell_state.get_var(&var_name) {
280                        result.push_str(&value);
281                    } else {
282                        // Variable not found - for positional parameters, expand to empty string
283                        // For other variables, keep the literal
284                        if var_name.chars().next().unwrap().is_ascii_digit()
285                            || var_name == "?"
286                            || var_name == "$"
287                            || var_name == "0"
288                            || var_name == "#"
289                            || var_name == "*"
290                            || var_name == "@"
291                        {
292                            // Expand to empty string for undefined positional parameters
293                        } else {
294                            // Keep the literal for regular variables
295                            result.push_str("${");
296                            result.push_str(&var_name);
297                            result.push('}');
298                        }
299                    }
300                } else {
301                    // Malformed ${...} - keep as literal
302                    result.push_str("${");
303                    result.push_str(&var_name);
304                    if !found_closing {
305                        // No closing brace found
306                    }
307                }
308            } else {
309                // Regular variable
310                let mut var_name = String::new();
311                let mut next_ch = chars.peek();
312
313                // Handle special single-character variables first
314                if let Some(&c) = next_ch {
315                    if c == '?' || c == '$' || c == '0' || c == '#' || c == '*' || c == '@' {
316                        var_name.push(c);
317                        chars.next(); // consume the character
318                    } else if c.is_ascii_digit() {
319                        // Positional parameter
320                        var_name.push(c);
321                        chars.next();
322                    } else {
323                        // Regular variable name (including multi-character special variables like LINENO)
324                        while let Some(&c) = next_ch {
325                            if c.is_alphanumeric() || c == '_' {
326                                var_name.push(c);
327                                chars.next(); // consume the character
328                                next_ch = chars.peek();
329                            } else {
330                                break;
331                            }
332                        }
333                    }
334                }
335
336                if !var_name.is_empty() {
337                    if let Some(value) = shell_state.get_var(&var_name) {
338                        result.push_str(&value);
339                    } else {
340                        // Variable not found - for positional parameters, expand to empty string
341                        // For other variables, keep the literal
342                        if var_name.chars().next().unwrap().is_ascii_digit()
343                            || var_name == "?"
344                            || var_name == "$"
345                            || var_name == "0"
346                            || var_name == "#"
347                            || var_name == "*"
348                            || var_name == "@"
349                        {
350                            // Expand to empty string for undefined positional parameters
351                        } else {
352                            // Keep the literal for regular variables
353                            result.push('$');
354                            result.push_str(&var_name);
355                        }
356                    }
357                } else {
358                    result.push('$');
359                }
360            }
361        } else if ch == '`' {
362            // Backtick command substitution
363            let mut sub_command = String::new();
364
365            for c in chars.by_ref() {
366                if c == '`' {
367                    break;
368                }
369                sub_command.push(c);
370            }
371
372            // Execute the command substitution
373            if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
374                // Expand aliases before parsing
375                let expanded_tokens = match crate::lexer::expand_aliases(
376                    tokens,
377                    shell_state,
378                    &mut std::collections::HashSet::new(),
379                ) {
380                    Ok(t) => t,
381                    Err(_) => {
382                        // Alias expansion error, keep literal
383                        result.push('`');
384                        result.push_str(&sub_command);
385                        result.push('`');
386                        continue;
387                    }
388                };
389
390                if let Ok(ast) = crate::parser::parse(expanded_tokens) {
391                    // Execute and capture output
392                    match super::execute_and_capture_output(ast, shell_state) {
393                        Ok(output) => {
394                            result.push_str(&output);
395                        }
396                        Err(_) => {
397                            // On failure, keep the literal
398                            result.push('`');
399                            result.push_str(&sub_command);
400                            result.push('`');
401                        }
402                    }
403                } else {
404                    // Parse error, keep literal
405                    result.push('`');
406                    result.push_str(&sub_command);
407                    result.push('`');
408                }
409            } else {
410                // Lex error, keep literal
411                result.push('`');
412                result.push_str(&sub_command);
413                result.push('`');
414            }
415        } else {
416            result.push(ch);
417        }
418    }
419
420    result
421}
422
423/// Expand shell-style wildcard patterns in a list of arguments unless the `noglob` option is set.
424///
425/// 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.
426///
427/// # Examples
428///
429/// ```
430/// // Note: expand_wildcards is a private function
431/// // This example is for documentation only
432/// ```
433pub(crate) fn expand_wildcards(args: &[String], shell_state: &ShellState) -> Result<Vec<String>, String> {
434    let mut expanded_args = Vec::new();
435
436    for arg in args {
437        // Skip wildcard expansion if noglob option (-f) is enabled
438        if shell_state.options.noglob {
439            expanded_args.push(arg.clone());
440            continue;
441        }
442        
443        if arg.contains('*') || arg.contains('?') || arg.contains('[') {
444            // Try to expand wildcard
445            match glob::glob(arg) {
446                Ok(paths) => {
447                    let mut matches: Vec<String> = paths
448                        .filter_map(|p| p.ok())
449                        .map(|p| p.to_string_lossy().to_string())
450                        .collect();
451                    if matches.is_empty() {
452                        // No matches, keep literal
453                        expanded_args.push(arg.clone());
454                    } else {
455                        // Sort for consistent behavior
456                        matches.sort();
457                        expanded_args.extend(matches);
458                    }
459                }
460                Err(_e) => {
461                    // Invalid pattern, keep literal
462                    expanded_args.push(arg.clone());
463                }
464            }
465        } else {
466            expanded_args.push(arg.clone());
467        }
468    }
469    Ok(expanded_args)
470}