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}