| 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 |