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}