1pub mod fd_table;
3
4pub mod options;
6
7pub mod signals;
9
10pub mod jobs;
12
13pub use fd_table::FileDescriptorTable;
15pub use options::ShellOptions;
16pub use signals::{enqueue_signal, process_pending_signals, check_background_jobs};
17pub use jobs::{Job, JobStatus, JobTable};
18
19use super::parser::Ast;
20use std::cell::RefCell;
21use std::collections::{HashMap, HashSet};
22use std::env;
23use std::io::IsTerminal;
24use std::os::unix::io::RawFd;
25use std::rc::Rc;
26use std::sync::{Arc, Mutex};
27
28#[derive(Debug, Clone)]
29pub struct ColorScheme {
30 pub prompt: String,
32 pub error: String,
34 pub success: String,
36 pub builtin: String,
38 pub directory: String,
40}
41
42impl Default for ColorScheme {
43 fn default() -> Self {
44 Self {
45 prompt: "\x1b[32m".to_string(), error: "\x1b[31m".to_string(), success: "\x1b[32m".to_string(), builtin: "\x1b[36m".to_string(), directory: "\x1b[34m".to_string(), }
51 }
52}
53
54#[derive(Debug, Clone)]
55pub struct ShellState {
56 pub variables: HashMap<String, String>,
58 pub exported: HashSet<String>,
60 pub last_exit_code: i32,
62 pub shell_pid: u32,
64 pub script_name: String,
66 pub dir_stack: Vec<String>,
68 pub aliases: HashMap<String, String>,
70 pub colors_enabled: bool,
72 pub color_scheme: ColorScheme,
74 pub positional_params: Vec<String>,
76 pub functions: HashMap<String, Ast>,
78 pub local_vars: Vec<HashMap<String, String>>,
80 pub function_depth: usize,
82 pub max_recursion_depth: usize,
84 pub returning: bool,
86 pub return_value: Option<i32>,
88 pub loop_depth: usize,
90 pub breaking: bool,
92 pub break_level: usize,
94 pub continuing: bool,
96 pub continue_level: usize,
98 pub capture_output: Option<Rc<RefCell<Vec<u8>>>>,
100 pub condensed_cwd: bool,
102 pub trap_handlers: Arc<Mutex<HashMap<String, String>>>,
104 pub exit_trap_executed: bool,
106 pub exit_requested: bool,
108 pub exit_code: i32,
110 #[allow(dead_code)]
113 pub pending_signals: bool,
114 pub pending_heredoc_content: Option<String>,
116 pub collecting_heredoc: Option<(String, String, String)>, pub fd_table: Rc<RefCell<FileDescriptorTable>>,
120 pub subshell_depth: usize,
122 pub stdin_override: Option<RawFd>,
124 pub options: ShellOptions,
126 pub in_condition: bool,
128 pub in_logical_chain: bool,
130 pub in_negation: bool,
132 pub last_was_negation: bool,
134 pub current_line_number: usize,
136 pub line_number_stack: Vec<usize>,
138 pub job_table: Rc<RefCell<JobTable>>,
140 pub interactive: bool,
142 pub terminal_fd: Option<RawFd>,
144 pub last_background_pid: Option<u32>,
146}
147
148impl ShellState {
149 pub fn new() -> Self {
165 let shell_pid = std::process::id();
166
167 let no_color = env::var("NO_COLOR").is_ok();
169
170 let rush_colors = env::var("RUSH_COLORS")
172 .map(|v| v.to_lowercase())
173 .unwrap_or_else(|_| "auto".to_string());
174
175 let colors_enabled = match rush_colors.as_str() {
176 "1" | "true" | "on" | "enable" => !no_color && std::io::stdout().is_terminal(),
177 "0" | "false" | "off" | "disable" => false,
178 "auto" => !no_color && std::io::stdout().is_terminal(),
179 _ => !no_color && std::io::stdout().is_terminal(),
180 };
181
182 let rush_condensed = env::var("RUSH_CONDENSED")
184 .map(|v| v.to_lowercase())
185 .unwrap_or_else(|_| "true".to_string());
186
187 let condensed_cwd = match rush_condensed.as_str() {
188 "1" | "true" | "on" | "enable" => true,
189 "0" | "false" | "off" | "disable" => false,
190 _ => true, };
192
193 Self {
194 variables: HashMap::new(),
195 exported: HashSet::new(),
196 last_exit_code: 0,
197 shell_pid,
198 script_name: "rush".to_string(),
199 dir_stack: Vec::new(),
200 aliases: HashMap::new(),
201 colors_enabled,
202 color_scheme: ColorScheme::default(),
203 positional_params: Vec::new(),
204 functions: HashMap::new(),
205 local_vars: Vec::new(),
206 function_depth: 0,
207 max_recursion_depth: 500, returning: false,
209 return_value: None,
210 loop_depth: 0,
211 breaking: false,
212 break_level: 0,
213 continuing: false,
214 continue_level: 0,
215 capture_output: None,
216 condensed_cwd,
217 trap_handlers: Arc::new(Mutex::new(HashMap::new())),
218 exit_trap_executed: false,
219 exit_requested: false,
220 exit_code: 0,
221 pending_signals: false,
222 pending_heredoc_content: None,
223 collecting_heredoc: None,
224 fd_table: Rc::new(RefCell::new(FileDescriptorTable::new())),
225 subshell_depth: 0,
226 stdin_override: None,
227 options: ShellOptions::default(),
228 in_condition: false,
229 in_logical_chain: false,
230 in_negation: false,
231 last_was_negation: false,
232 current_line_number: 1,
233 line_number_stack: Vec::new(),
234 job_table: Rc::new(RefCell::new(JobTable::new())),
235 interactive: std::io::stdin().is_terminal(),
236 terminal_fd: if std::io::stdin().is_terminal() {
237 Some(libc::STDIN_FILENO)
238 } else {
239 None
240 },
241 last_background_pid: None,
242 }
243 }
244
245 pub fn get_var(&self, name: &str) -> Option<String> {
247 match name {
249 "?" => Some(self.last_exit_code.to_string()),
250 "$" => Some(self.shell_pid.to_string()),
251 "0" => Some(self.script_name.clone()),
252 "!" => self.last_background_pid.map(|pid| pid.to_string()),
253 "LINENO" => Some(self.current_line_number.to_string()),
254 "*" => {
255 if self.positional_params.is_empty() {
257 Some("".to_string())
258 } else {
259 Some(self.positional_params.join(" "))
260 }
261 }
262 "@" => {
263 if self.positional_params.is_empty() {
265 Some("".to_string())
266 } else {
267 Some(self.positional_params.join(" "))
268 }
269 }
270 "#" => Some(self.positional_params.len().to_string()),
271 _ => {
272 if let Ok(index) = name.parse::<usize>()
274 && index > 0
275 && index <= self.positional_params.len()
276 {
277 return Some(self.positional_params[index - 1].clone());
278 }
279
280 for scope in self.local_vars.iter().rev() {
283 if let Some(value) = scope.get(name) {
284 return Some(value.clone());
285 }
286 }
287
288 if let Some(value) = self.variables.get(name) {
290 Some(value.clone())
291 } else {
292 env::var(name).ok()
294 }
295 }
296 }
297 }
298
299 pub fn set_var(&mut self, name: &str, value: String) {
301 for scope in self.local_vars.iter_mut().rev() {
304 if scope.contains_key(name) {
305 scope.insert(name.to_string(), value);
306 return;
307 }
308 }
309
310 self.variables.insert(name.to_string(), value);
312 }
313
314 pub fn unset_var(&mut self, name: &str) {
316 self.variables.remove(name);
317 self.exported.remove(name);
318 }
319
320 pub fn export_var(&mut self, name: &str) {
322 if self.variables.contains_key(name) {
323 self.exported.insert(name.to_string());
324 }
325 }
326
327 pub fn set_exported_var(&mut self, name: &str, value: String) {
329 self.set_var(name, value);
330 self.export_var(name);
331 }
332
333 pub fn get_env_for_child(&self) -> HashMap<String, String> {
335 let mut child_env = HashMap::new();
336
337 for (key, value) in env::vars() {
339 child_env.insert(key, value);
340 }
341
342 for var_name in &self.exported {
344 if let Some(value) = self.variables.get(var_name) {
345 child_env.insert(var_name.clone(), value.clone());
346 }
347 }
348
349 child_env
350 }
351
352 pub fn set_last_exit_code(&mut self, code: i32) {
354 self.last_exit_code = code;
355 }
356
357 pub fn set_script_name(&mut self, name: &str) {
359 self.script_name = name.to_string();
360 }
361
362 pub fn get_condensed_cwd(&self) -> String {
364 match env::current_dir() {
365 Ok(path) => {
366 let path_str = path.to_string_lossy();
367 let components: Vec<&str> = path_str.split('/').collect();
368 if components.is_empty() || (components.len() == 1 && components[0].is_empty()) {
369 return "/".to_string();
370 }
371 let mut result = String::new();
372 for (i, comp) in components.iter().enumerate() {
373 if comp.is_empty() {
374 continue; }
376 if i == components.len() - 1 {
377 result.push('/');
378 result.push_str(comp);
379 } else {
380 result.push('/');
381 if let Some(first) = comp.chars().next() {
382 result.push(first);
383 }
384 }
385 }
386 if result.is_empty() {
387 "/".to_string()
388 } else {
389 result
390 }
391 }
392 Err(_) => "/?".to_string(), }
394 }
395
396 pub fn get_full_cwd(&self) -> String {
398 match env::current_dir() {
399 Ok(path) => path.to_string_lossy().to_string(),
400 Err(_) => "/?".to_string(), }
402 }
403
404 pub fn get_user_hostname(&self) -> String {
406 let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
407
408 if let Ok(hostname) = env::var("HOSTNAME")
410 && !hostname.trim().is_empty()
411 {
412 return format!("{}@{}", user, hostname);
413 }
414
415 let hostname = match std::process::Command::new("hostname").output() {
417 Ok(output) if output.status.success() => {
418 String::from_utf8_lossy(&output.stdout).trim().to_string()
419 }
420 _ => "hostname".to_string(), };
422
423 if hostname != "hostname" {
425 unsafe {
426 env::set_var("HOSTNAME", &hostname);
427 }
428 }
429
430 format!("{}@{}", user, hostname)
431 }
432
433 pub fn get_prompt(&self) -> String {
435 let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
436 let prompt_char = if user == "root" { "#" } else { "$" };
437 let cwd = if self.condensed_cwd {
438 self.get_condensed_cwd()
439 } else {
440 self.get_full_cwd()
441 };
442 format!("{}:{} {} ", self.get_user_hostname(), cwd, prompt_char)
443 }
444
445 pub fn set_alias(&mut self, name: &str, value: String) {
447 self.aliases.insert(name.to_string(), value);
448 }
449
450 pub fn get_alias(&self, name: &str) -> Option<&String> {
452 self.aliases.get(name)
453 }
454
455 pub fn remove_alias(&mut self, name: &str) {
457 self.aliases.remove(name);
458 }
459
460 pub fn get_all_aliases(&self) -> &HashMap<String, String> {
462 &self.aliases
463 }
464
465 pub fn set_positional_params(&mut self, params: Vec<String>) {
467 self.positional_params = params;
468 }
469
470 #[allow(dead_code)]
472 pub fn get_positional_params(&self) -> &[String] {
473 &self.positional_params
474 }
475
476 pub fn shift_positional_params(&mut self, count: usize) {
478 if count > 0 {
479 for _ in 0..count {
480 if !self.positional_params.is_empty() {
481 self.positional_params.remove(0);
482 }
483 }
484 }
485 }
486
487 #[allow(dead_code)]
489 pub fn push_positional_param(&mut self, param: String) {
490 self.positional_params.push(param);
491 }
492
493 pub fn define_function(&mut self, name: String, body: Ast) {
495 self.functions.insert(name, body);
496 }
497
498 pub fn get_function(&self, name: &str) -> Option<&Ast> {
500 self.functions.get(name)
501 }
502
503 #[allow(dead_code)]
505 pub fn remove_function(&mut self, name: &str) {
506 self.functions.remove(name);
507 }
508
509 #[allow(dead_code)]
511 pub fn get_function_names(&self) -> Vec<&String> {
512 self.functions.keys().collect()
513 }
514
515 pub fn push_local_scope(&mut self) {
517 self.local_vars.push(HashMap::new());
518 }
519
520 pub fn pop_local_scope(&mut self) {
522 if !self.local_vars.is_empty() {
523 self.local_vars.pop();
524 }
525 }
526
527 pub fn set_local_var(&mut self, name: &str, value: String) {
529 if let Some(current_scope) = self.local_vars.last_mut() {
530 current_scope.insert(name.to_string(), value);
531 } else {
532 self.set_var(name, value);
534 }
535 }
536
537 pub fn enter_function(&mut self) {
539 self.function_depth += 1;
540 if self.function_depth > self.local_vars.len() {
541 self.push_local_scope();
542 }
543 }
544
545 pub fn exit_function(&mut self) {
547 if self.function_depth > 0 {
548 self.function_depth -= 1;
549 if self.function_depth == self.local_vars.len() - 1 {
550 self.pop_local_scope();
551 }
552 }
553 }
554
555 pub fn set_return(&mut self, value: i32) {
557 self.returning = true;
558 self.return_value = Some(value);
559 }
560
561 pub fn clear_return(&mut self) {
563 self.returning = false;
564 self.return_value = None;
565 }
566
567 pub fn is_returning(&self) -> bool {
569 self.returning
570 }
571
572 pub fn get_return_value(&self) -> Option<i32> {
574 self.return_value
575 }
576
577 pub fn enter_loop(&mut self) {
579 self.loop_depth += 1;
580 }
581
582 pub fn exit_loop(&mut self) {
584 if self.loop_depth > 0 {
585 self.loop_depth -= 1;
586 }
587 }
588
589 pub fn set_break(&mut self, level: usize) {
591 self.breaking = true;
592 self.break_level = level;
593 }
594
595 pub fn clear_break(&mut self) {
597 self.breaking = false;
598 self.break_level = 0;
599 }
600
601 pub fn is_breaking(&self) -> bool {
603 self.breaking
604 }
605
606 pub fn get_break_level(&self) -> usize {
608 self.break_level
609 }
610
611 pub fn decrement_break_level(&mut self) {
613 if self.break_level > 0 {
614 self.break_level -= 1;
615 }
616 if self.break_level == 0 {
617 self.breaking = false;
618 }
619 }
620
621 pub fn set_continue(&mut self, level: usize) {
623 self.continuing = true;
624 self.continue_level = level;
625 }
626
627 pub fn clear_continue(&mut self) {
629 self.continuing = false;
630 self.continue_level = 0;
631 }
632
633 pub fn is_continuing(&self) -> bool {
635 self.continuing
636 }
637
638 pub fn get_continue_level(&self) -> usize {
640 self.continue_level
641 }
642
643 pub fn decrement_continue_level(&mut self) {
645 if self.continue_level > 0 {
646 self.continue_level -= 1;
647 }
648 if self.continue_level == 0 {
649 self.continuing = false;
650 }
651 }
652
653 pub fn set_trap(&mut self, signal: &str, command: String) {
655 if let Ok(mut handlers) = self.trap_handlers.lock() {
656 handlers.insert(signal.to_uppercase(), command);
657 }
658 }
659
660 pub fn get_trap(&self, signal: &str) -> Option<String> {
662 if let Ok(handlers) = self.trap_handlers.lock() {
663 handlers.get(&signal.to_uppercase()).cloned()
664 } else {
665 None
666 }
667 }
668
669 pub fn remove_trap(&mut self, signal: &str) {
671 if let Ok(mut handlers) = self.trap_handlers.lock() {
672 handlers.remove(&signal.to_uppercase());
673 }
674 }
675
676 pub fn get_all_traps(&self) -> HashMap<String, String> {
678 if let Ok(handlers) = self.trap_handlers.lock() {
679 handlers.clone()
680 } else {
681 HashMap::new()
682 }
683 }
684
685 #[allow(dead_code)]
687 pub fn clear_traps(&mut self) {
688 if let Ok(mut handlers) = self.trap_handlers.lock() {
689 handlers.clear();
690 }
691 }
692}
693
694impl Default for ShellState {
695 fn default() -> Self {
696 Self::new()
697 }
698}
699
700#[cfg(test)]
701mod tests;