Rust · 22037 bytes Raw Blame History
1 use rush_expand::Context;
2 use rush_parser::{CaseStatement, CompleteCommand, ForStatement, IfStatement, WhileStatement};
3 use rush_parser::ast::{CondExpr, SelectStatement, Word};
4 use crate::{ExecutionError, ExecutionResult, PipelineError};
5 use regex::Regex;
6 use globset::Glob;
7
8 /// Execute an if statement
9 pub fn execute_if(
10 if_stmt: &IfStatement,
11 context: &mut Context,
12 ) -> Result<ExecutionResult, PipelineError> {
13 // Execute the condition and check if it succeeded
14 let condition_result = execute_complete_command(&if_stmt.condition, context)?;
15
16 if condition_result.success() {
17 // Condition was true, execute then body
18 execute_command_list(&if_stmt.then_body, context)
19 } else {
20 // Check elif clauses
21 for elif in &if_stmt.elif_clauses {
22 let elif_result = execute_complete_command(&elif.condition, context)?;
23 if elif_result.success() {
24 return execute_command_list(&elif.then_body, context);
25 }
26 }
27
28 // No elif matched, execute else body if present
29 if let Some(else_body) = &if_stmt.else_body {
30 execute_command_list(else_body, context)
31 } else {
32 // No else clause, return success (standard shell behavior)
33 Ok(crate::command::success_result())
34 }
35 }
36 }
37
38 /// Execute a while loop
39 pub fn execute_while(
40 while_stmt: &WhileStatement,
41 context: &mut Context,
42 ) -> Result<ExecutionResult, PipelineError> {
43 let mut last_result = crate::command::success_result();
44
45 loop {
46 // Execute the condition
47 let condition_result = execute_complete_command(&while_stmt.condition, context)?;
48
49 if !condition_result.success() {
50 // Condition failed, exit loop
51 break;
52 }
53
54 // Execute the loop body
55 match execute_command_list(&while_stmt.body, context) {
56 Ok(result) => last_result = result,
57 Err(PipelineError::Break) => break,
58 Err(PipelineError::Continue) => continue,
59 Err(e) => return Err(e),
60 }
61 }
62
63 Ok(last_result)
64 }
65
66 /// Execute a for loop
67 pub fn execute_for(
68 for_stmt: &ForStatement,
69 context: &mut Context,
70 ) -> Result<ExecutionResult, PipelineError> {
71 use rush_expand::expand_word;
72
73 let mut last_result = crate::command::success_result();
74
75 // Expand all the words in the list
76 let mut values = Vec::new();
77 for word in &for_stmt.words {
78 let expanded = expand_word(word, context)
79 .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
80 values.push(expanded);
81 }
82
83 // Execute the loop body for each value
84 for value in values {
85 // Set the loop variable
86 if let Err(name) = context.set_var(&for_stmt.var_name, &value) {
87 return Err(PipelineError::IoError(std::io::Error::new(
88 std::io::ErrorKind::PermissionDenied,
89 format!("{}: readonly variable", name),
90 )));
91 }
92
93 // Execute the loop body
94 match execute_command_list(&for_stmt.body, context) {
95 Ok(result) => last_result = result,
96 Err(PipelineError::Break) => break,
97 Err(PipelineError::Continue) => continue,
98 Err(e) => return Err(e),
99 }
100 }
101
102 Ok(last_result)
103 }
104
105 /// Check if a string matches a glob pattern (for case statement matching)
106 fn matches_pattern(text: &str, pattern: &str) -> bool {
107 // Try to compile the glob pattern
108 match Glob::new(pattern) {
109 Ok(glob) => {
110 let matcher = glob.compile_matcher();
111 matcher.is_match(text)
112 }
113 Err(_) => {
114 // If pattern is invalid, fall back to exact string matching
115 text == pattern
116 }
117 }
118 }
119
120 /// Execute a case statement
121 pub fn execute_case(
122 case_stmt: &CaseStatement,
123 context: &mut Context,
124 ) -> Result<ExecutionResult, PipelineError> {
125 use rush_expand::expand_word;
126
127 // Expand the word to match against
128 let word_value = expand_word(&case_stmt.word, context)
129 .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
130
131 // Try each case clause
132 for clause in &case_stmt.clauses {
133 // Check if any pattern matches
134 for pattern in &clause.patterns {
135 let pattern_value = expand_word(pattern, context)
136 .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
137
138 // Use glob pattern matching
139 if matches_pattern(&word_value, &pattern_value) {
140 // Pattern matched, execute this clause's commands
141 return execute_command_list(&clause.body, context);
142 }
143 }
144 }
145
146 // No clause matched, return success (standard shell behavior)
147 Ok(crate::command::success_result())
148 }
149
150 /// Execute a select loop
151 ///
152 /// The select loop displays a numbered menu and reads user input:
153 /// ```bash
154 /// select var in option1 option2 option3; do
155 /// echo "Selected: $var"
156 /// done
157 /// ```
158 pub fn execute_select(
159 select_stmt: &SelectStatement,
160 context: &mut Context,
161 ) -> Result<ExecutionResult, PipelineError> {
162 use rush_expand::expand_word;
163 use std::io::{self, BufRead, Write};
164
165 let mut last_result = crate::command::success_result();
166
167 // Expand all the words to get menu items
168 let mut items = Vec::new();
169 for word in &select_stmt.words {
170 let expanded = expand_word(word, context)
171 .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
172 items.push(expanded);
173 }
174
175 // If no items, return immediately
176 if items.is_empty() {
177 return Ok(last_result);
178 }
179
180 // Get PS3 prompt (default: "#? ") - converted to owned String to avoid borrow conflicts
181 let ps3 = context.get_var("PS3").unwrap_or("#? ").to_string();
182
183 // Calculate column width for menu display
184 let num_width = items.len().to_string().len();
185
186 loop {
187 // Display the menu to stderr (standard bash behavior)
188 for (i, item) in items.iter().enumerate() {
189 eprintln!("{:>width$}) {}", i + 1, item, width = num_width);
190 }
191
192 // Print the prompt and flush
193 eprint!("{}", ps3);
194 io::stderr().flush().ok();
195
196 // Read user input
197 let stdin = io::stdin();
198 let mut line = String::new();
199 match stdin.lock().read_line(&mut line) {
200 Ok(0) => {
201 // EOF - exit the loop
202 break;
203 }
204 Ok(_) => {
205 let input = line.trim();
206
207 // Store raw input in REPLY
208 let _ = context.set_var("REPLY", input);
209
210 // Parse the number
211 if let Ok(num) = input.parse::<usize>() {
212 if num >= 1 && num <= items.len() {
213 // Valid selection - set the variable
214 if let Err(name) = context.set_var(&select_stmt.var_name, &items[num - 1]) {
215 return Err(PipelineError::IoError(std::io::Error::new(
216 std::io::ErrorKind::PermissionDenied,
217 format!("{}: readonly variable", name),
218 )));
219 }
220 } else {
221 // Invalid number - set variable to empty
222 let _ = context.set_var(&select_stmt.var_name, "");
223 }
224 } else {
225 // Not a number - set variable to empty
226 let _ = context.set_var(&select_stmt.var_name, "");
227 }
228
229 // Execute the loop body
230 match execute_command_list(&select_stmt.body, context) {
231 Ok(result) => last_result = result,
232 Err(PipelineError::Break) => break,
233 Err(PipelineError::Continue) => continue,
234 Err(e) => return Err(e),
235 }
236 }
237 Err(e) => {
238 return Err(PipelineError::IoError(e));
239 }
240 }
241 }
242
243 Ok(last_result)
244 }
245
246 /// Execute a complete command (helper for recursive execution)
247 pub(crate) fn execute_complete_command(
248 cmd: &CompleteCommand,
249 context: &mut Context,
250 ) -> Result<ExecutionResult, PipelineError> {
251 use rush_parser::ast::CommandType;
252
253 // Handle background execution
254 #[cfg(unix)]
255 if cmd.background {
256 return execute_background(cmd, context);
257 }
258
259 match &cmd.command {
260 CommandType::Simple(simple_cmd) => {
261 Ok(crate::execute_simple_with_redirects(simple_cmd, context, false)?)
262 }
263 CommandType::Pipeline(pipeline) => crate::execute_pipeline(pipeline, context),
264 CommandType::AndOrList(and_or_list) => crate::execute_and_or_list(and_or_list, context),
265 CommandType::If(if_stmt) => execute_if(if_stmt, context),
266 CommandType::While(while_stmt) => execute_while(while_stmt, context),
267 CommandType::For(for_stmt) => execute_for(for_stmt, context),
268 CommandType::Case(case_stmt) => execute_case(case_stmt, context),
269 CommandType::Function(function_def) => {
270 // Store function in context
271 context.functions.insert(function_def.name.clone(), function_def.clone());
272 Ok(crate::command::success_result())
273 }
274 CommandType::Subshell(subshell) => {
275 crate::execute_subshell(subshell, context).map_err(|e| {
276 PipelineError::ExecutionError(ExecutionError::CommandNotFound(e))
277 })
278 }
279 CommandType::ExtendedTest(cond_expr) => {
280 execute_extended_test(cond_expr, context)
281 }
282 CommandType::Select(select_stmt) => execute_select(select_stmt, context),
283 }
284 }
285
286 /// Execute an extended test [[ expression ]]
287 pub fn execute_extended_test(
288 expr: &CondExpr,
289 context: &mut Context,
290 ) -> Result<ExecutionResult, PipelineError> {
291 let result = evaluate_cond_expr(expr, context)?;
292 Ok(crate::command::exit_code_to_result(if result { 0 } else { 1 }))
293 }
294
295 /// Evaluate a conditional expression and return true/false
296 fn evaluate_cond_expr(expr: &CondExpr, context: &mut Context) -> Result<bool, PipelineError> {
297 match expr {
298 CondExpr::Or(left, right) => {
299 // Short-circuit OR
300 if evaluate_cond_expr(left, context)? {
301 Ok(true)
302 } else {
303 evaluate_cond_expr(right, context)
304 }
305 }
306 CondExpr::And(left, right) => {
307 // Short-circuit AND
308 if !evaluate_cond_expr(left, context)? {
309 Ok(false)
310 } else {
311 evaluate_cond_expr(right, context)
312 }
313 }
314 CondExpr::Not(inner) => {
315 Ok(!evaluate_cond_expr(inner, context)?)
316 }
317 CondExpr::Unary { op, operand } => {
318 let value = expand_word(operand, context)?;
319 evaluate_unary_test(op, &value)
320 }
321 CondExpr::Binary { left, op, right } => {
322 let left_val = expand_word(left, context)?;
323 let right_val = expand_word(right, context)?;
324 evaluate_binary_test(op, &left_val, &right_val, context)
325 }
326 CondExpr::Word(word) => {
327 // Single word is true if non-empty
328 let value = expand_word(word, context)?;
329 Ok(!value.is_empty())
330 }
331 }
332 }
333
334 /// Helper to expand a word
335 fn expand_word(word: &Word, context: &mut Context) -> Result<String, PipelineError> {
336 rush_expand::expand_word(word, context)
337 .map_err(|e| PipelineError::ExpansionError(e.to_string()))
338 }
339
340 /// Evaluate unary test operator
341 fn evaluate_unary_test(op: &str, value: &str) -> Result<bool, PipelineError> {
342 use std::fs;
343 use std::os::unix::fs::FileTypeExt;
344
345 match op {
346 "-z" => Ok(value.is_empty()),
347 "-n" => Ok(!value.is_empty()),
348 "-e" => Ok(fs::metadata(value).is_ok()),
349 "-f" => Ok(fs::metadata(value).map(|m| m.is_file()).unwrap_or(false)),
350 "-d" => Ok(fs::metadata(value).map(|m| m.is_dir()).unwrap_or(false)),
351 "-r" => {
352 use std::os::unix::fs::PermissionsExt;
353 Ok(fs::metadata(value).map(|m| m.permissions().mode() & 0o444 != 0).unwrap_or(false))
354 }
355 "-w" => {
356 use std::os::unix::fs::PermissionsExt;
357 Ok(fs::metadata(value).map(|m| m.permissions().mode() & 0o222 != 0).unwrap_or(false))
358 }
359 "-x" => {
360 use std::os::unix::fs::PermissionsExt;
361 Ok(fs::metadata(value).map(|m| m.permissions().mode() & 0o111 != 0).unwrap_or(false))
362 }
363 "-s" => Ok(fs::metadata(value).map(|m| m.len() > 0).unwrap_or(false)),
364 "-L" | "-h" => Ok(fs::symlink_metadata(value).map(|m| m.file_type().is_symlink()).unwrap_or(false)),
365 "-p" => Ok(fs::metadata(value).map(|m| m.file_type().is_fifo()).unwrap_or(false)),
366 "-S" => Ok(fs::metadata(value).map(|m| m.file_type().is_socket()).unwrap_or(false)),
367 "-b" => Ok(fs::metadata(value).map(|m| m.file_type().is_block_device()).unwrap_or(false)),
368 "-c" => Ok(fs::metadata(value).map(|m| m.file_type().is_char_device()).unwrap_or(false)),
369 _ => Err(PipelineError::ExpansionError(format!("Unknown unary operator: {}", op))),
370 }
371 }
372
373 /// Evaluate binary test operator
374 fn evaluate_binary_test(op: &str, left: &str, right: &str, context: &mut Context) -> Result<bool, PipelineError> {
375 match op {
376 "==" | "=" => {
377 // Pattern matching (glob-style)
378 if right.contains('*') || right.contains('?') || right.contains('[') {
379 let pattern = format!("^{}$", glob_to_regex(right));
380 let re = Regex::new(&pattern)
381 .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
382 Ok(re.is_match(left))
383 } else {
384 Ok(left == right)
385 }
386 }
387 "!=" => {
388 if right.contains('*') || right.contains('?') || right.contains('[') {
389 let pattern = format!("^{}$", glob_to_regex(right));
390 let re = Regex::new(&pattern)
391 .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
392 Ok(!re.is_match(left))
393 } else {
394 Ok(left != right)
395 }
396 }
397 "=~" => {
398 // Regex matching, sets BASH_REMATCH
399 match Regex::new(right) {
400 Ok(re) => {
401 if let Some(captures) = re.captures(left) {
402 // Set BASH_REMATCH array
403 let matches: Vec<String> = captures
404 .iter()
405 .map(|m| m.map(|m| m.as_str().to_string()).unwrap_or_default())
406 .collect();
407 context.arrays.insert(
408 "BASH_REMATCH".to_string(),
409 rush_expand::context::ArrayType::Indexed(matches),
410 );
411 Ok(true)
412 } else {
413 // Clear BASH_REMATCH on no match
414 context.arrays.remove("BASH_REMATCH");
415 Ok(false)
416 }
417 }
418 Err(_) => {
419 context.arrays.remove("BASH_REMATCH");
420 Ok(false)
421 }
422 }
423 }
424 "<" => Ok(left < right),
425 ">" => Ok(left > right),
426 "-eq" => {
427 let l: i64 = left.parse().unwrap_or(0);
428 let r: i64 = right.parse().unwrap_or(0);
429 Ok(l == r)
430 }
431 "-ne" => {
432 let l: i64 = left.parse().unwrap_or(0);
433 let r: i64 = right.parse().unwrap_or(0);
434 Ok(l != r)
435 }
436 "-lt" => {
437 let l: i64 = left.parse().unwrap_or(0);
438 let r: i64 = right.parse().unwrap_or(0);
439 Ok(l < r)
440 }
441 "-le" => {
442 let l: i64 = left.parse().unwrap_or(0);
443 let r: i64 = right.parse().unwrap_or(0);
444 Ok(l <= r)
445 }
446 "-gt" => {
447 let l: i64 = left.parse().unwrap_or(0);
448 let r: i64 = right.parse().unwrap_or(0);
449 Ok(l > r)
450 }
451 "-ge" => {
452 let l: i64 = left.parse().unwrap_or(0);
453 let r: i64 = right.parse().unwrap_or(0);
454 Ok(l >= r)
455 }
456 "-nt" => {
457 // Newer than
458 use std::fs;
459 let left_time = fs::metadata(left).and_then(|m| m.modified()).ok();
460 let right_time = fs::metadata(right).and_then(|m| m.modified()).ok();
461 Ok(match (left_time, right_time) {
462 (Some(l), Some(r)) => l > r,
463 (Some(_), None) => true,
464 _ => false,
465 })
466 }
467 "-ot" => {
468 // Older than
469 use std::fs;
470 let left_time = fs::metadata(left).and_then(|m| m.modified()).ok();
471 let right_time = fs::metadata(right).and_then(|m| m.modified()).ok();
472 Ok(match (left_time, right_time) {
473 (Some(l), Some(r)) => l < r,
474 (None, Some(_)) => true,
475 _ => false,
476 })
477 }
478 "-ef" => {
479 // Same file (same device and inode)
480 use std::os::unix::fs::MetadataExt;
481 use std::fs;
482 let left_meta = fs::metadata(left).ok();
483 let right_meta = fs::metadata(right).ok();
484 Ok(match (left_meta, right_meta) {
485 (Some(l), Some(r)) => l.dev() == r.dev() && l.ino() == r.ino(),
486 _ => false,
487 })
488 }
489 _ => Err(PipelineError::ExpansionError(format!("Unknown binary operator: {}", op))),
490 }
491 }
492
493 /// Convert glob pattern to regex
494 fn glob_to_regex(pattern: &str) -> String {
495 let mut result = String::new();
496 let mut chars = pattern.chars().peekable();
497
498 while let Some(ch) = chars.next() {
499 match ch {
500 '*' => result.push_str(".*"),
501 '?' => result.push('.'),
502 '[' => {
503 result.push('[');
504 // Handle character class
505 while let Some(&c) = chars.peek() {
506 chars.next();
507 if c == ']' {
508 result.push(']');
509 break;
510 }
511 result.push(c);
512 }
513 }
514 '.' | '+' | '^' | '$' | '(' | ')' | '{' | '}' | '|' | '\\' => {
515 result.push('\\');
516 result.push(ch);
517 }
518 _ => result.push(ch),
519 }
520 }
521
522 result
523 }
524
525 #[cfg(unix)]
526 fn execute_background(
527 cmd: &CompleteCommand,
528 context: &mut Context,
529 ) -> Result<ExecutionResult, PipelineError> {
530 use rush_parser::ast::CommandType;
531 use std::process::{Command, Stdio};
532 use std::os::unix::process::CommandExt;
533 use nix::unistd::{setpgid, Pid};
534
535 // For now, only support simple commands and pipelines in background
536 match &cmd.command {
537 CommandType::Simple(simple_cmd) => {
538 // Expand the command
539 let expanded = rush_expand::expand_words(&simple_cmd.words, context)
540 .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
541
542 if expanded.is_empty() {
543 return Ok(crate::command::success_result());
544 }
545
546 let command_name = &expanded[0];
547 let args = &expanded[1..];
548
549 // Build the command
550 let program_path = crate::command::find_in_path(command_name)
551 .ok_or_else(|| crate::ExecutionError::CommandNotFound(
552 rush_interactive::ErrorHints::command_not_found(command_name)
553 ))?;
554
555 let mut command = Command::new(program_path);
556 command.args(args);
557
558 // Background jobs: stdin from /dev/null, stdout/stderr inherited
559 command.stdin(Stdio::null());
560 command.stdout(Stdio::inherit());
561 command.stderr(Stdio::inherit());
562
563 // Set up process group
564 unsafe {
565 command.pre_exec(|| {
566 // Put the child in its own process group
567 setpgid(Pid::from_raw(0), Pid::from_raw(0))?;
568 Ok(())
569 });
570 }
571
572 // Spawn the child process
573 let child = command.spawn()
574 .map_err(|e| PipelineError::IoError(e))?;
575 let child_pid = Pid::from_raw(child.id() as i32);
576
577 // Set the child's process group (belt and suspenders)
578 let _ = setpgid(child_pid, child_pid);
579
580 // Add to job list
581 let command_str = expanded.join(" ");
582 let job_id = context.job_list.add_job(
583 child_pid, // pgid = pid for simple commands
584 command_str.clone(),
585 vec![child_pid],
586 false, // not foreground
587 );
588
589 // Print job notification
590 println!("[{}] {}", job_id, child_pid);
591
592 // Return success immediately (don't wait)
593 Ok(crate::command::success_result())
594 }
595 _ => {
596 // For now, don't support complex commands in background
597 // Fall back to foreground execution
598 eprintln!("rush: background execution of complex commands not yet supported");
599 execute_complete_command(&CompleteCommand::foreground(cmd.command.clone()), context)
600 }
601 }
602 }
603
604 /// Execute a list of commands
605 fn execute_command_list(
606 commands: &[CompleteCommand],
607 context: &mut Context,
608 ) -> Result<ExecutionResult, PipelineError> {
609 let mut last_result = crate::command::success_result();
610
611 for cmd in commands {
612 last_result = execute_complete_command(cmd, context)?;
613 context.set_exit_status(last_result.exit_code());
614 }
615
616 Ok(last_result)
617 }
618