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 == '!'
115                                        || c.is_ascii_digit()
116                                    {
117                                        var_name.push(c);
118                                        expr_chars.next();
119                                    } else {
120                                        while let Some(&c) = expr_chars.peek() {
121                                            if c.is_alphanumeric() || c == '_' {
122                                                var_name.push(c);
123                                                expr_chars.next();
124                                            } else {
125                                                break;
126                                            }
127                                        }
128                                    }
129                                }
130
131                                if !var_name.is_empty() {
132                                    if let Some(value) = shell_state.get_var(&var_name) {
133                                        expanded_expr.push_str(&value);
134                                    } else {
135                                        // Variable not found, use 0 for arithmetic
136                                        expanded_expr.push('0');
137                                    }
138                                } else {
139                                    expanded_expr.push('$');
140                                }
141                            } else {
142                                expanded_expr.push(ch);
143                            }
144                        }
145
146                        match crate::arithmetic::evaluate_arithmetic_expression(
147                            &expanded_expr,
148                            shell_state,
149                        ) {
150                            Ok(value) => {
151                                result.push_str(&value.to_string());
152                            }
153                            Err(e) => {
154                                // On arithmetic error, display a proper error message
155                                if shell_state.colors_enabled {
156                                    result.push_str(&format!(
157                                        "{}arithmetic error: {}{}",
158                                        shell_state.color_scheme.error, e, "\x1b[0m"
159                                    ));
160                                } else {
161                                    result.push_str(&format!("arithmetic error: {}", e));
162                                }
163                            }
164                        }
165                    } else {
166                        // Didn't find proper closing - keep as literal
167                        result.push_str("$((");
168                        result.push_str(&arithmetic_expr);
169                        // Note: we don't add closing parens since they weren't in the input
170                    }
171                    continue;
172                }
173
174                // Regular command substitution $(...)
175                let mut sub_command = String::new();
176                let mut paren_depth = 1;
177
178                for c in chars.by_ref() {
179                    if c == '(' {
180                        paren_depth += 1;
181                        sub_command.push(c);
182                    } else if c == ')' {
183                        paren_depth -= 1;
184                        if paren_depth == 0 {
185                            break;
186                        }
187                        sub_command.push(c);
188                    } else {
189                        sub_command.push(c);
190                    }
191                }
192
193                // Execute the command substitution within the current shell context
194                // Parse and execute the command using our own lexer/parser/executor
195                if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
196                    // Expand aliases before parsing
197                    let expanded_tokens = match crate::lexer::expand_aliases(
198                        tokens,
199                        shell_state,
200                        &mut std::collections::HashSet::new(),
201                    ) {
202                        Ok(t) => t,
203                        Err(_) => {
204                            // Alias expansion error, keep literal
205                            result.push_str("$(");
206                            result.push_str(&sub_command);
207                            result.push(')');
208                            continue;
209                        }
210                    };
211
212                    match crate::parser::parse(expanded_tokens) {
213                        Ok(ast) => {
214                            // Execute within current shell context and capture output
215                            match super::execute_and_capture_output(ast, shell_state) {
216                                Ok(output) => {
217                                    result.push_str(&output);
218                                }
219                                Err(_) => {
220                                    // On failure, keep the literal
221                                    result.push_str("$(");
222                                    result.push_str(&sub_command);
223                                    result.push(')');
224                                }
225                            }
226                        }
227                        Err(_parse_err) => {
228                            // Parse error - try to handle as function call if it looks like one
229                            let tokens_str = sub_command.trim();
230                            if tokens_str.contains(' ') {
231                                // Split by spaces and check if first token looks like a function call
232                                let parts: Vec<&str> = tokens_str.split_whitespace().collect();
233                                if let Some(first_token) = parts.first()
234                                    && shell_state.get_function(first_token).is_some()
235                                {
236                                    // This is a function call, create AST manually
237                                    let function_call = Ast::FunctionCall {
238                                        name: first_token.to_string(),
239                                        args: parts[1..].iter().map(|s| s.to_string()).collect(),
240                                    };
241                                    match super::execute_and_capture_output(function_call, shell_state) {
242                                        Ok(output) => {
243                                            result.push_str(&output);
244                                            continue;
245                                        }
246                                        Err(_) => {
247                                            // Fall back to literal
248                                        }
249                                    }
250                                }
251                            }
252                            // Keep the literal
253                            result.push_str("$(");
254                            result.push_str(&sub_command);
255                            result.push(')');
256                        }
257                    }
258                } else {
259                    // Lex error, keep literal
260                    result.push_str("$(");
261                    result.push_str(&sub_command);
262                    result.push(')');
263                }
264            } else if let Some(&'{') = chars.peek() {
265                // ${VAR} syntax
266                chars.next(); // consume the {
267                let mut var_name = String::new();
268                let mut found_closing = false;
269
270                // Read until we find the closing }
271                for c in chars.by_ref() {
272                    if c == '}' {
273                        found_closing = true;
274                        break;
275                    }
276                    var_name.push(c);
277                }
278
279                if found_closing && !var_name.is_empty() {
280                    if let Some(value) = shell_state.get_var(&var_name) {
281                        result.push_str(&value);
282                    } else {
283                        // Variable not found - for positional parameters and special variables, expand to empty string
284                        // For other variables, keep the literal
285                        if var_name.chars().next().unwrap().is_ascii_digit()
286                            || var_name == "?"
287                            || var_name == "$"
288                            || var_name == "0"
289                            || var_name == "#"
290                            || var_name == "*"
291                            || var_name == "@"
292                            || var_name == "!"
293                        {
294                            // Expand to empty string for undefined positional parameters and special variables
295                        } else {
296                            // Keep the literal for regular variables
297                            result.push_str("${");
298                            result.push_str(&var_name);
299                            result.push('}');
300                        }
301                    }
302                } else {
303                    // Malformed ${...} - keep as literal
304                    result.push_str("${");
305                    result.push_str(&var_name);
306                    if !found_closing {
307                        // No closing brace found
308                    }
309                }
310            } else {
311                // Regular variable
312                let mut var_name = String::new();
313                let mut next_ch = chars.peek();
314
315                // Handle special single-character variables first
316                if let Some(&c) = next_ch {
317                    if c == '?' || c == '$' || c == '0' || c == '#' || c == '*' || c == '@' || c == '!' {
318                        var_name.push(c);
319                        chars.next(); // consume the character
320                    } else if c.is_ascii_digit() {
321                        // Positional parameter
322                        var_name.push(c);
323                        chars.next();
324                    } else {
325                        // Regular variable name (including multi-character special variables like LINENO)
326                        while let Some(&c) = next_ch {
327                            if c.is_alphanumeric() || c == '_' {
328                                var_name.push(c);
329                                chars.next(); // consume the character
330                                next_ch = chars.peek();
331                            } else {
332                                break;
333                            }
334                        }
335                    }
336                }
337
338                if !var_name.is_empty() {
339                    if let Some(value) = shell_state.get_var(&var_name) {
340                        result.push_str(&value);
341                    } else {
342                        // Variable not found - for positional parameters and special variables, expand to empty string
343                        // For other variables, keep the literal
344                        if var_name.chars().next().unwrap().is_ascii_digit()
345                            || var_name == "?"
346                            || var_name == "$"
347                            || var_name == "0"
348                            || var_name == "#"
349                            || var_name == "*"
350                            || var_name == "@"
351                            || var_name == "!"
352                        {
353                            // Expand to empty string for undefined positional parameters and special variables
354                        } else {
355                            // Keep the literal for regular variables
356                            result.push('$');
357                            result.push_str(&var_name);
358                        }
359                    }
360                } else {
361                    result.push('$');
362                }
363            }
364        } else if ch == '`' {
365            // Backtick command substitution
366            let mut sub_command = String::new();
367
368            for c in chars.by_ref() {
369                if c == '`' {
370                    break;
371                }
372                sub_command.push(c);
373            }
374
375            // Execute the command substitution
376            if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
377                // Expand aliases before parsing
378                let expanded_tokens = match crate::lexer::expand_aliases(
379                    tokens,
380                    shell_state,
381                    &mut std::collections::HashSet::new(),
382                ) {
383                    Ok(t) => t,
384                    Err(_) => {
385                        // Alias expansion error, keep literal
386                        result.push('`');
387                        result.push_str(&sub_command);
388                        result.push('`');
389                        continue;
390                    }
391                };
392
393                if let Ok(ast) = crate::parser::parse(expanded_tokens) {
394                    // Execute and capture output
395                    match super::execute_and_capture_output(ast, shell_state) {
396                        Ok(output) => {
397                            result.push_str(&output);
398                        }
399                        Err(_) => {
400                            // On failure, keep the literal
401                            result.push('`');
402                            result.push_str(&sub_command);
403                            result.push('`');
404                        }
405                    }
406                } else {
407                    // Parse error, keep literal
408                    result.push('`');
409                    result.push_str(&sub_command);
410                    result.push('`');
411                }
412            } else {
413                // Lex error, keep literal
414                result.push('`');
415                result.push_str(&sub_command);
416                result.push('`');
417            }
418        } else {
419            result.push(ch);
420        }
421    }
422
423    result
424}
425
426/// Expand shell-style wildcard patterns in a list of arguments unless the `noglob` option is set.
427///
428/// 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.
429///
430/// # Examples
431///
432/// ```
433/// // Note: expand_wildcards is a private function
434/// // This example is for documentation only
435/// ```
436pub(crate) fn expand_wildcards(args: &[String], shell_state: &ShellState) -> Result<Vec<String>, String> {
437    let mut expanded_args = Vec::new();
438
439    for arg in args {
440        // Skip wildcard expansion if noglob option (-f) is enabled
441        if shell_state.options.noglob {
442            expanded_args.push(arg.clone());
443            continue;
444        }
445        
446        if arg.contains('*') || arg.contains('?') || arg.contains('[') {
447            // Try to expand wildcard
448            match glob::glob(arg) {
449                Ok(paths) => {
450                    let mut matches: Vec<String> = paths
451                        .filter_map(|p| p.ok())
452                        .map(|p| p.to_string_lossy().to_string())
453                        .collect();
454                    if matches.is_empty() {
455                        // No matches, keep literal
456                        expanded_args.push(arg.clone());
457                    } else {
458                        // Sort for consistent behavior
459                        matches.sort();
460                        expanded_args.extend(matches);
461                    }
462                }
463                Err(_e) => {
464                    // Invalid pattern, keep literal
465                    expanded_args.push(arg.clone());
466                }
467            }
468        } else {
469            expanded_args.push(arg.clone());
470        }
471    }
472    Ok(expanded_args)
473}