Rust · 19098 bytes Raw Blame History
1 use clap::Parser;
2 use rush_expand::{Context, CommandExecutorWrapper};
3 use std::fs;
4 use std::io::{self, IsTerminal, Read};
5 use std::process::ExitCode;
6 use std::rc::Rc;
7
8 mod heredoc;
9 mod repl;
10
11 /// Create a new Context with internal command substitution enabled
12 pub(crate) fn create_context() -> Context {
13 let mut context = Context::new();
14
15 // Set up internal command executor for command substitution
16 // This allows $(command) to execute using rush's own parser/executor
17 // instead of delegating to sh -c
18 context.command_executor = CommandExecutorWrapper(Some(Rc::new(rush_executor::execute_for_substitution)));
19
20 context
21 }
22
23 #[derive(Parser)]
24 #[command(name = "rush")]
25 #[command(about = "The Rust Shell", long_about = None)]
26 struct Cli {
27 /// Execute command string
28 #[arg(short = 'c', value_name = "COMMAND")]
29 command: Option<String>,
30
31 /// Run as a login shell
32 #[arg(short = 'l', long = "login")]
33 login: bool,
34
35 /// Script file to execute
36 #[arg(value_name = "FILE")]
37 file: Option<String>,
38 }
39
40 fn main() -> ExitCode {
41 // Register cleanup handler for process substitutions
42 // This will clean up any remaining FIFOs when the shell exits
43 #[cfg(unix)]
44 {
45 extern "C" fn cleanup_on_exit() {
46 rush_executor::cleanup_process_substs();
47 }
48 unsafe {
49 nix::libc::atexit(cleanup_on_exit);
50 }
51 }
52
53 // Set up signal handling for the shell
54 if let Err(e) = rush_executor::setup_shell_signals() {
55 eprintln!("rush: failed to set up signal handlers: {}", e);
56 return ExitCode::from(1);
57 }
58
59 let cli = Cli::parse();
60
61 // Determine execution mode
62 if let Some(command) = cli.command {
63 // Execute command string mode: rush -c "command"
64 execute_string(&command)
65 } else if let Some(file) = cli.file {
66 // Execute script file mode: rush script.sh
67 execute_file(&file)
68 } else if io::stdin().is_terminal() {
69 // Interactive mode (terminal)
70 // Set up job control for interactive mode
71 #[cfg(unix)]
72 if let Err(e) = setup_interactive_shell() {
73 eprintln!("rush: warning: failed to set up job control: {}", e);
74 }
75
76 // Check if this is a login shell (either -l flag or argv[0] starts with '-')
77 let is_login = cli.login || std::env::args().next()
78 .map(|arg| arg.starts_with('-'))
79 .unwrap_or(false);
80
81 repl::run_interactive(is_login)
82 } else {
83 // Non-interactive mode (stdin)
84 execute_stdin()
85 }
86 }
87
88 /// Set up the shell for interactive use with job control
89 #[cfg(unix)]
90 fn setup_interactive_shell() -> Result<(), String> {
91 use rush_job::{save_terminal_attrs, setup_job_control_signals, setup_shell_terminal};
92
93 // Save terminal attributes for later restoration
94 save_terminal_attrs().map_err(|e| format!("save terminal attrs: {}", e))?;
95
96 // Put shell in its own process group and take terminal control
97 setup_shell_terminal().map_err(|e| e.to_string())?;
98
99 // Set up job control signals (SIGWINCH, SIGHUP, SIGQUIT, etc.)
100 setup_job_control_signals().map_err(|e| format!("job control signals: {}", e))?;
101
102 Ok(())
103 }
104
105 /// Check for completed or stopped background jobs and update their status
106 #[cfg(unix)]
107 pub(crate) fn check_background_jobs(context: &mut Context) {
108 use rush_job::check_children;
109
110 // Check for any children that have changed state
111 for (pid, status) in check_children() {
112 // Update the job in the job list
113 if let Some(job_id) = context.job_list.update_job_status(pid, status) {
114 // Print notification for completed jobs
115 if let Some(job) = context.job_list.get_job(job_id) {
116 if job.is_completed() {
117 println!("[{}] Done {}", job.id, job.command);
118 } else if job.is_stopped() {
119 println!("[{}] Stopped {}", job.id, job.command);
120 }
121 }
122 }
123 }
124
125 // Clean up completed jobs
126 context.job_list.clean_completed();
127 }
128
129 /// Execute a command string
130 fn execute_string(command: &str) -> ExitCode {
131 let mut context = create_context();
132 match execute_line(command, &mut context, false) {
133 Ok(code) => ExitCode::from(code as u8),
134 Err(e) => {
135 eprintln!("rush: {}", e);
136 ExitCode::from(1)
137 }
138 }
139 }
140
141 /// Execute a script file
142 fn execute_file(path: &str) -> ExitCode {
143 let content = match fs::read_to_string(path) {
144 Ok(c) => c,
145 Err(e) => {
146 eprintln!("rush: {}: {}", path, e);
147 return ExitCode::from(1);
148 }
149 };
150
151 let mut context = create_context();
152
153 // Split into lines for heredoc support and multiline statements
154 let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
155 let mut lines_iter = lines.into_iter().peekable();
156
157 let mut last_exit_code = 0;
158
159 // Accumulate complete statements before execution
160 while lines_iter.peek().is_some() {
161 // Accumulate lines until we have a complete statement
162 let mut statement_lines = Vec::new();
163
164 // Skip leading empty lines and comments
165 while let Some(line) = lines_iter.peek() {
166 if line.trim().is_empty() || line.trim().starts_with('#') {
167 lines_iter.next();
168 } else {
169 break;
170 }
171 }
172
173 if lines_iter.peek().is_none() {
174 break;
175 }
176
177 // Accumulate lines until statement is complete (balanced braces)
178 let mut brace_depth = 0;
179 let mut in_single_quote = false;
180 let mut in_double_quote = false;
181 let mut saw_opening_brace = false;
182
183 loop {
184 let line = match lines_iter.next() {
185 Some(l) => l,
186 None => break,
187 };
188
189 // Track brace depth (ignoring braces in strings)
190 let mut chars = line.chars().peekable();
191 let mut escaped = false;
192
193 while let Some(ch) = chars.next() {
194 if escaped {
195 escaped = false;
196 continue;
197 }
198
199 match ch {
200 '\\' => escaped = true,
201 '\'' if !in_double_quote => in_single_quote = !in_single_quote,
202 '"' if !in_single_quote => in_double_quote = !in_double_quote,
203 '{' if !in_single_quote && !in_double_quote => {
204 brace_depth += 1;
205 saw_opening_brace = true;
206 }
207 '}' if !in_single_quote && !in_double_quote => brace_depth -= 1,
208 _ => {}
209 }
210 }
211
212 statement_lines.push(line);
213
214 // Check if statement is complete
215 // - Must have balanced braces (depth 0)
216 // - If line ends with ), we need to see { on a following line
217 // - If line starts with "function" but no {, we need to see { on a following line
218 let trimmed = statement_lines.last().unwrap().trim();
219 let ends_with_paren = trimmed.ends_with(')') && !trimmed.contains('{');
220 let is_function_without_brace = trimmed.starts_with("function ") && !trimmed.contains('{');
221
222 if brace_depth == 0 && !statement_lines.is_empty() {
223 // If last line ends with ), wait for the opening brace
224 if ends_with_paren && !saw_opening_brace {
225 continue;
226 }
227 // If line starts with "function" but no brace, wait for it
228 if is_function_without_brace && !saw_opening_brace {
229 continue;
230 }
231 break;
232 }
233 }
234
235 if statement_lines.is_empty() {
236 continue;
237 }
238
239 // Join the accumulated lines
240 let statement = statement_lines.join("\n");
241
242 // Execute the complete statement with heredoc support
243 match execute_statement_with_heredocs(&statement, &mut lines_iter, &mut context, false) {
244 Ok(code) => last_exit_code = code,
245 Err(e) => {
246 eprintln!("rush: {}", e);
247 return ExitCode::from(1);
248 }
249 }
250 }
251
252 ExitCode::from(last_exit_code as u8)
253 }
254
255 /// Execute commands from stdin
256 fn execute_stdin() -> ExitCode {
257 let stdin = io::stdin();
258 let mut content = String::new();
259
260 // Read all of stdin at once to support multi-line control flow
261 if let Err(e) = stdin.lock().read_to_string(&mut content) {
262 eprintln!("rush: error reading stdin: {}", e);
263 return ExitCode::from(1);
264 }
265
266 let mut context = create_context();
267 match execute_line(&content, &mut context, false) {
268 Ok(code) => ExitCode::from(code as u8),
269 Err(e) => {
270 eprintln!("rush: {}", e);
271 ExitCode::from(1)
272 }
273 }
274 }
275
276 /// Execute a single line of shell input
277 fn execute_line(line: &str, context: &mut Context, interactive: bool) -> Result<i32, String> {
278 // For single-line execution, heredocs won't work (no way to get more lines)
279 execute_statement_with_heredocs(line, &mut std::iter::empty(), context, interactive)
280 }
281
282 /// Execute a statement, optionally reading heredoc content from lines
283 fn execute_statement_with_heredocs<I>(
284 line: &str,
285 lines: &mut I,
286 context: &mut Context,
287 interactive: bool,
288 ) -> Result<i32, String>
289 where
290 I: Iterator<Item = String>,
291 {
292 use rush_parser::{parse_line, Statement};
293
294 let mut statement = parse_line(line).map_err(|e| e.to_string())?;
295
296 // Check if we need to collect heredoc content
297 if heredoc::has_heredocs(&statement) {
298 let delimiters = heredoc::get_heredoc_delimiters(&statement);
299 let mut content_map = std::collections::HashMap::new();
300
301 // Collect content for each heredoc
302 for delimiter in delimiters {
303 let mut content_lines = Vec::new();
304
305 // Read lines until we find the delimiter
306 for line in lines.by_ref() {
307 if line.trim() == delimiter {
308 break;
309 }
310 content_lines.push(line);
311 }
312
313 content_map.insert(delimiter, content_lines);
314 }
315
316 // Fill the content into the statement
317 heredoc::fill_heredoc_content(&mut statement, &content_map);
318 }
319
320 match statement {
321 Statement::Empty => Ok(0),
322 Statement::Complete(complete_cmd) => {
323 execute_complete_command(&complete_cmd, context, interactive)
324 }
325 Statement::Script(commands) => {
326 let mut last_exit_code = 0;
327 for cmd in commands {
328 last_exit_code = execute_complete_command(&cmd, context, interactive)?;
329 }
330 Ok(last_exit_code)
331 }
332 }
333 }
334
335 fn execute_complete_command(
336 cmd: &rush_parser::CompleteCommand,
337 context: &mut Context,
338 interactive: bool,
339 ) -> Result<i32, String> {
340 use rush_executor::{
341 execute_and_or_list, execute_case, execute_for, execute_if, execute_pipeline,
342 execute_select, execute_simple_with_redirects, execute_subshell, execute_while,
343 };
344 use rush_parser::ast::CommandType;
345
346 // Handle background execution
347 if cmd.background {
348 return execute_background_command(cmd, context);
349 }
350
351 // Foreground execution (normal case)
352 let result = match &cmd.command {
353 CommandType::Simple(simple_cmd) => {
354 execute_simple_with_redirects(simple_cmd, context, interactive)
355 .map_err(|e| e.to_string())?
356 }
357 CommandType::Pipeline(pipeline) => {
358 execute_pipeline(pipeline, context)
359 .map_err(|e| e.to_string())?
360 }
361 CommandType::AndOrList(and_or_list) => {
362 execute_and_or_list(and_or_list, context)
363 .map_err(|e| e.to_string())?
364 }
365 CommandType::If(if_stmt) => {
366 execute_if(if_stmt, context)
367 .map_err(|e| e.to_string())?
368 }
369 CommandType::While(while_stmt) => {
370 execute_while(while_stmt, context)
371 .map_err(|e| e.to_string())?
372 }
373 CommandType::For(for_stmt) => {
374 execute_for(for_stmt, context)
375 .map_err(|e| e.to_string())?
376 }
377 CommandType::Case(case_stmt) => {
378 execute_case(case_stmt, context)
379 .map_err(|e| e.to_string())?
380 }
381 CommandType::Select(select_stmt) => {
382 execute_select(select_stmt, context)
383 .map_err(|e| e.to_string())?
384 }
385 CommandType::Function(function_def) => {
386 // Store function in context
387 context.functions.insert(function_def.name.clone(), function_def.clone());
388 // Return success
389 rush_executor::command::ExecutionResult {
390 exit_status: std::process::ExitStatus::default(),
391 #[cfg(unix)]
392 job_control: None,
393 }
394 }
395 CommandType::Subshell(subshell) => {
396 execute_subshell(subshell, context)
397 .map_err(|e| e.to_string())?
398 }
399 CommandType::ExtendedTest(cond) => {
400 rush_executor::control_flow::execute_extended_test(cond, context)
401 .map_err(|e| e.to_string())?
402 }
403 };
404
405 // Check if the job was stopped (Ctrl-Z)
406 #[cfg(unix)]
407 if result.is_stopped() {
408 if let Some(job_control) = &result.job_control {
409 // Construct command string from the AST
410 let command_string = build_command_string(&cmd.command, context);
411
412 // Add to job list
413 let job_id = context.job_list.add_job(
414 job_control.pgid,
415 command_string.clone(),
416 vec![job_control.pid],
417 false, // not foreground anymore
418 );
419
420 // Print notification
421 eprintln!("[{}]+ Stopped {}", job_id, command_string);
422 }
423 }
424
425 let exit_code = result.exit_code();
426 context.set_exit_status(exit_code);
427 Ok(exit_code)
428 }
429
430 /// Build a command string from AST for display purposes
431 #[cfg(unix)]
432 fn build_command_string(cmd: &rush_parser::ast::CommandType, context: &mut Context) -> String {
433 use rush_parser::ast::CommandType;
434
435 match cmd {
436 CommandType::Simple(simple_cmd) => {
437 // Expand words to get the command as it was executed
438 if let Ok(expanded) = rush_expand::expand_words(&simple_cmd.words, context) {
439 expanded.join(" ")
440 } else {
441 // Fallback if expansion fails
442 "<command>".to_string()
443 }
444 }
445 CommandType::Pipeline(pipeline) => {
446 let parts: Vec<String> = pipeline.commands.iter()
447 .filter_map(|elem| {
448 match elem {
449 rush_parser::ast::PipelineElement::Simple(simple_cmd) => {
450 if let Ok(expanded) = rush_expand::expand_words(&simple_cmd.words, context) {
451 if !expanded.is_empty() {
452 Some(expanded.join(" "))
453 } else {
454 None
455 }
456 } else {
457 None
458 }
459 }
460 rush_parser::ast::PipelineElement::Subshell(_) => {
461 Some("(subshell)".to_string())
462 }
463 rush_parser::ast::PipelineElement::ExtendedTest(_) => {
464 Some("[[ test ]]".to_string())
465 }
466 }
467 })
468 .collect();
469 parts.join(" | ")
470 }
471 _ => {
472 // For control flow commands, just show a generic label
473 // In practice, these won't be stopped in the foreground in Phase 5
474 "command".to_string()
475 }
476 }
477 }
478
479 /// Execute a command in the background
480 #[cfg(unix)]
481 fn execute_background_command(
482 cmd: &rush_parser::CompleteCommand,
483 context: &mut Context,
484 ) -> Result<i32, String> {
485 use rush_executor::{execute_pipeline_background, execute_simple_background, execute_subshell_background};
486 use rush_parser::ast::CommandType;
487
488 match &cmd.command {
489 CommandType::Simple(simple_cmd) => {
490 // Execute in background
491 let (pid, pgid, command_string) = execute_simple_background(simple_cmd, context)
492 .map_err(|e| e.to_string())?;
493
494 // Add to job list
495 let job_id = context.job_list.add_job(
496 pgid,
497 command_string.clone(),
498 vec![pid],
499 false, // not foreground
500 );
501
502 // Print job notification
503 println!("[{}] {}", job_id, pid);
504
505 // Background jobs return success immediately
506 Ok(0)
507 }
508 CommandType::Pipeline(pipeline) => {
509 // Execute pipeline in background
510 let (pids, pgid, command_string) = execute_pipeline_background(pipeline, context)
511 .map_err(|e| e.to_string())?;
512
513 // Add to job list
514 let job_id = context.job_list.add_job(
515 pgid,
516 command_string.clone(),
517 pids.clone(),
518 false, // not foreground
519 );
520
521 // Print job notification (show last PID in pipeline)
522 println!("[{}] {}", job_id, pids.last().unwrap());
523
524 // Background jobs return success immediately
525 Ok(0)
526 }
527 CommandType::Subshell(subshell) => {
528 // Execute subshell in background
529 let (pid, pgid, command_string) = execute_subshell_background(subshell, context)
530 .map_err(|e| e.to_string())?;
531
532 // Add to job list
533 let job_id = context.job_list.add_job(
534 pgid,
535 command_string.clone(),
536 vec![pid],
537 false, // not foreground
538 );
539
540 // Print job notification
541 println!("[{}] {}", job_id, pid);
542
543 // Background jobs return success immediately
544 Ok(0)
545 }
546 _ => {
547 // Other control flow commands in background not yet supported
548 Err("Background execution of control flow commands not yet supported".to_string())
549 }
550 }
551 }
552
553 #[cfg(not(unix))]
554 fn execute_background_command(
555 _cmd: &rush_parser::CompleteCommand,
556 _context: &mut Context,
557 ) -> Result<i32, String> {
558 Err("Background execution not supported on this platform".to_string())
559 }
560
561 // Make execute_line available to the repl module
562 pub(crate) fn execute_interactive_line(line: &str, context: &mut Context) -> Result<(), String> {
563 execute_line(line, context, true).map(|_| ())
564 }
565