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}