rush_sh/
state.rs

1use super::parser::Ast;
2use lazy_static::lazy_static;
3use std::cell::RefCell;
4use std::collections::{HashMap, HashSet, VecDeque};
5use std::env;
6use std::fs::{File, OpenOptions};
7use std::io::IsTerminal;
8use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
9use std::process::Stdio;
10use std::rc::Rc;
11use std::sync::{Arc, Mutex};
12use std::time::Instant;
13
14lazy_static! {
15    /// Global queue for pending signal events
16    /// Signals are enqueued by the signal handler thread and dequeued by the main thread
17    pub static ref SIGNAL_QUEUE: Arc<Mutex<VecDeque<SignalEvent>>> =
18        Arc::new(Mutex::new(VecDeque::new()));
19}
20
21/// Maximum number of signals to queue before dropping old ones
22const MAX_SIGNAL_QUEUE_SIZE: usize = 100;
23
24/// Represents a signal event that needs to be processed
25#[derive(Debug, Clone)]
26pub struct SignalEvent {
27    /// Signal name (e.g., "INT", "TERM")
28    pub signal_name: String,
29    /// Signal number (e.g., 2, 15)
30    pub signal_number: i32,
31    /// When the signal was received
32    pub timestamp: Instant,
33}
34
35impl SignalEvent {
36    pub fn new(signal_name: String, signal_number: i32) -> Self {
37        Self {
38            signal_name,
39            signal_number,
40            timestamp: Instant::now(),
41        }
42    }
43}
44
45/// Represents an open file descriptor
46#[derive(Debug)]
47pub enum FileDescriptor {
48    /// Standard file opened for reading, writing, or both
49    File(File),
50    /// Duplicate of another file descriptor
51    Duplicate(RawFd),
52    /// Closed file descriptor
53    Closed,
54}
55
56impl FileDescriptor {
57    pub fn try_clone(&self) -> Result<Self, String> {
58        match self {
59            FileDescriptor::File(f) => {
60                let new_file = f
61                    .try_clone()
62                    .map_err(|e| format!("Failed to clone file: {}", e))?;
63                Ok(FileDescriptor::File(new_file))
64            }
65            FileDescriptor::Duplicate(fd) => Ok(FileDescriptor::Duplicate(*fd)),
66            FileDescriptor::Closed => Ok(FileDescriptor::Closed),
67        }
68    }
69}
70
71/// File descriptor table for managing open file descriptors
72#[derive(Debug)]
73pub struct FileDescriptorTable {
74    /// Map of fd number to file descriptor
75    fds: HashMap<i32, FileDescriptor>,
76    /// Saved file descriptors for restoration after command execution
77    saved_fds: HashMap<i32, RawFd>,
78}
79
80impl FileDescriptorTable {
81    /// Create a new empty file descriptor table
82    pub fn new() -> Self {
83        Self {
84            fds: HashMap::new(),
85            saved_fds: HashMap::new(),
86        }
87    }
88
89    /// Open a file and assign it to a file descriptor number
90    ///
91    /// # Arguments
92    /// * `fd_num` - The file descriptor number (0-9)
93    /// * `path` - Path to the file to open
94    /// * `read` - Whether to open for reading
95    /// * `write` - Whether to open for writing
96    /// * `append` - Whether to open in append mode
97    /// * `truncate` - Whether to truncate the file
98    ///
99    /// # Returns
100    /// * `Ok(())` on success
101    /// * `Err(String)` with error message on failure
102    pub fn open_fd(
103        &mut self,
104        fd_num: i32,
105        path: &str,
106        read: bool,
107        write: bool,
108        append: bool,
109        truncate: bool,
110    ) -> Result<(), String> {
111        // Validate fd number
112        if !(0..=1024).contains(&fd_num) {
113            return Err(format!("Invalid file descriptor number: {}", fd_num));
114        }
115
116        // Open the file with the specified options
117        let file = OpenOptions::new()
118            .read(read)
119            .write(write)
120            .append(append)
121            .truncate(truncate)
122            .create(write || append)
123            .open(path)
124            .map_err(|e| format!("Cannot open {}: {}", path, e))?;
125
126        // Store the file descriptor
127        self.fds.insert(fd_num, FileDescriptor::File(file));
128        Ok(())
129    }
130
131    /// Duplicate a file descriptor
132    ///
133    /// # Arguments
134    /// * `source_fd` - The source file descriptor to duplicate
135    /// * `target_fd` - The target file descriptor number
136    ///
137    /// # Returns
138    /// * `Ok(())` on success
139    /// * `Err(String)` with error message on failure
140    pub fn duplicate_fd(&mut self, source_fd: i32, target_fd: i32) -> Result<(), String> {
141        // Validate fd numbers
142        if !(0..=1024).contains(&source_fd) {
143            return Err(format!("Invalid source file descriptor: {}", source_fd));
144        }
145        if !(0..=1024).contains(&target_fd) {
146            return Err(format!("Invalid target file descriptor: {}", target_fd));
147        }
148
149        // POSIX: Duplicating to self is a no-op
150        if source_fd == target_fd {
151            return Ok(());
152        }
153
154        // Get the raw fd to duplicate
155        let raw_fd = match self.get_raw_fd(source_fd) {
156            Some(fd) => fd,
157            None => {
158                return Err(format!(
159                    "File descriptor {} is not open or is closed",
160                    source_fd
161                ));
162            }
163        };
164
165        // Store the duplication
166        self.fds
167            .insert(target_fd, FileDescriptor::Duplicate(raw_fd));
168        Ok(())
169    }
170
171    /// Close a file descriptor
172    ///
173    /// # Arguments
174    /// * `fd_num` - The file descriptor number to close
175    ///
176    /// # Returns
177    /// * `Ok(())` on success
178    /// * `Err(String)` with error message on failure
179    pub fn close_fd(&mut self, fd_num: i32) -> Result<(), String> {
180        // Validate fd number
181        if !(0..=1024).contains(&fd_num) {
182            return Err(format!("Invalid file descriptor number: {}", fd_num));
183        }
184
185        // Mark the fd as closed
186        self.fds.insert(fd_num, FileDescriptor::Closed);
187        Ok(())
188    }
189
190    /// Save the current state of a file descriptor for later restoration
191    ///
192    /// # Arguments
193    /// * `fd_num` - The file descriptor number to save
194    ///
195    /// # Returns
196    /// * `Ok(())` on success
197    /// * `Err(String)` with error message on failure
198    pub fn save_fd(&mut self, fd_num: i32) -> Result<(), String> {
199        // Validate fd number
200        if !(0..=1024).contains(&fd_num) {
201            return Err(format!("Invalid file descriptor number: {}", fd_num));
202        }
203
204        // Duplicate the fd using dup() syscall to save it
205        let saved_fd = unsafe {
206            let raw_fd = fd_num as RawFd;
207            libc::dup(raw_fd)
208        };
209
210        if saved_fd < 0 {
211            return Err(format!("Failed to save file descriptor {}", fd_num));
212        }
213
214        self.saved_fds.insert(fd_num, saved_fd);
215        Ok(())
216    }
217
218    /// Restore a previously saved file descriptor
219    ///
220    /// # Arguments
221    /// * `fd_num` - The file descriptor number to restore
222    ///
223    /// # Returns
224    /// * `Ok(())` on success
225    /// * `Err(String)` with error message on failure
226    pub fn restore_fd(&mut self, fd_num: i32) -> Result<(), String> {
227        // Validate fd number
228        if !(0..=1024).contains(&fd_num) {
229            return Err(format!("Invalid file descriptor number: {}", fd_num));
230        }
231
232        // Get the saved fd
233        if let Some(saved_fd) = self.saved_fds.remove(&fd_num) {
234            // Restore using dup2() syscall
235            unsafe {
236                let result = libc::dup2(saved_fd, fd_num as RawFd);
237                libc::close(saved_fd); // Close the saved fd
238
239                if result < 0 {
240                    return Err(format!("Failed to restore file descriptor {}", fd_num));
241                }
242            }
243
244            // Remove from our tracking
245            self.fds.remove(&fd_num);
246        }
247
248        Ok(())
249    }
250
251    /// Create a deep copy of the file descriptor table
252    /// This duplicates all open file descriptors so they are independent of the original table
253    pub fn deep_clone(&self) -> Result<Self, String> {
254        let mut new_fds = HashMap::new();
255        for (fd, descriptor) in &self.fds {
256            new_fds.insert(*fd, descriptor.try_clone()?);
257        }
258
259        Ok(Self {
260            fds: new_fds,
261            saved_fds: self.saved_fds.clone(),
262        })
263    }
264
265    /// Save all currently open file descriptors
266    ///
267    /// # Returns
268    /// * `Ok(())` on success
269    /// * `Err(String)` with error message on failure
270    pub fn save_all_fds(&mut self) -> Result<(), String> {
271        // Save all fds that we're tracking
272        let fd_nums: Vec<i32> = self.fds.keys().copied().collect();
273        for fd_num in fd_nums {
274            self.save_fd(fd_num)?;
275        }
276
277        // Also explicitly save standard FDs (0, 1, 2) if they aren't already tracked
278        // This ensures changes to standard streams (via CommandGroup etc.) can be restored
279        for fd in 0..=2 {
280            if !self.fds.contains_key(&fd) {
281                // Try to save, ignore error if fd is closed/invalid
282                let _ = self.save_fd(fd);
283            }
284        }
285        Ok(())
286    }
287
288    /// Restore all previously saved file descriptors
289    ///
290    /// # Returns
291    /// * `Ok(())` on success
292    /// * `Err(String)` with error message on failure
293    pub fn restore_all_fds(&mut self) -> Result<(), String> {
294        // Restore all saved fds
295        let fd_nums: Vec<i32> = self.saved_fds.keys().copied().collect();
296        for fd_num in fd_nums {
297            self.restore_fd(fd_num)?;
298        }
299        Ok(())
300    }
301
302    /// Get a file handle for a given file descriptor number
303    ///
304    /// # Arguments
305    /// * `fd_num` - The file descriptor number
306    ///
307    /// # Returns
308    /// * `Some(Stdio)` if the fd is open and can be converted to Stdio
309    /// * `None` if the fd is not open or is closed
310    #[allow(dead_code)]
311    pub fn get_stdio(&self, fd_num: i32) -> Option<Stdio> {
312        match self.fds.get(&fd_num) {
313            Some(FileDescriptor::File(file)) => {
314                // Try to duplicate the file descriptor for Stdio
315                let raw_fd = file.as_raw_fd();
316                let dup_fd = unsafe { libc::dup(raw_fd) };
317                if dup_fd >= 0 {
318                    let file = unsafe { File::from_raw_fd(dup_fd) };
319                    Some(Stdio::from(file))
320                } else {
321                    None
322                }
323            }
324            Some(FileDescriptor::Duplicate(raw_fd)) => {
325                // Duplicate the raw fd for Stdio
326                let dup_fd = unsafe { libc::dup(*raw_fd) };
327                if dup_fd >= 0 {
328                    let file = unsafe { File::from_raw_fd(dup_fd) };
329                    Some(Stdio::from(file))
330                } else {
331                    None
332                }
333            }
334            Some(FileDescriptor::Closed) | None => None,
335        }
336    }
337
338    /// Get the raw file descriptor number for a given fd
339    ///
340    /// # Arguments
341    /// * `fd_num` - The file descriptor number
342    ///
343    /// # Returns
344    /// * `Some(RawFd)` if the fd is open
345    /// * `None` if the fd is not open or is closed
346    pub fn get_raw_fd(&self, fd_num: i32) -> Option<RawFd> {
347        match self.fds.get(&fd_num) {
348            Some(FileDescriptor::File(file)) => Some(file.as_raw_fd()),
349            Some(FileDescriptor::Duplicate(raw_fd)) => Some(*raw_fd),
350            Some(FileDescriptor::Closed) => None,
351            None => {
352                // Standard file descriptors (0, 1, 2) are always open unless explicitly closed
353                if fd_num >= 0 && fd_num <= 2 {
354                    Some(fd_num as RawFd)
355                } else {
356                    None
357                }
358            }
359        }
360    }
361
362    /// Check if a file descriptor is open
363    ///
364    /// # Arguments
365    /// * `fd_num` - The file descriptor number
366    ///
367    /// # Returns
368    /// * `true` if the fd is open
369    /// * `false` if the fd is closed or not tracked
370    pub fn is_open(&self, fd_num: i32) -> bool {
371        matches!(
372            self.fds.get(&fd_num),
373            Some(FileDescriptor::File(_)) | Some(FileDescriptor::Duplicate(_))
374        )
375    }
376
377    /// Check if a file descriptor is closed
378    ///
379    /// # Arguments
380    /// * `fd_num` - The file descriptor number
381    ///
382    /// # Returns
383    /// * `true` if the fd is explicitly closed
384    /// * `false` otherwise
385    pub fn is_closed(&self, fd_num: i32) -> bool {
386        matches!(self.fds.get(&fd_num), Some(FileDescriptor::Closed))
387    }
388
389    /// Clear all file descriptors and saved state
390    pub fn clear(&mut self) {
391        self.fds.clear();
392        self.saved_fds.clear();
393    }
394}
395
396impl Default for FileDescriptorTable {
397    fn default() -> Self {
398        Self::new()
399    }
400}
401
402#[derive(Debug, Clone)]
403pub struct ColorScheme {
404    /// ANSI color code for prompt
405    pub prompt: String,
406    /// ANSI color code for error messages
407    pub error: String,
408    /// ANSI color code for success messages
409    pub success: String,
410    /// ANSI color code for builtin command output
411    pub builtin: String,
412    /// ANSI color code for directory listings
413    pub directory: String,
414}
415
416impl Default for ColorScheme {
417    fn default() -> Self {
418        Self {
419            prompt: "\x1b[32m".to_string(),    // Green
420            error: "\x1b[31m".to_string(),     // Red
421            success: "\x1b[32m".to_string(),   // Green
422            builtin: "\x1b[36m".to_string(),   // Cyan
423            directory: "\x1b[34m".to_string(), // Blue
424        }
425    }
426}
427
428#[derive(Debug, Clone)]
429pub struct ShellState {
430    /// Shell variables (local to the shell session)
431    pub variables: HashMap<String, String>,
432    /// Which variables are exported to child processes
433    pub exported: HashSet<String>,
434    /// Last exit code ($?)
435    pub last_exit_code: i32,
436    /// Shell process ID ($$)
437    pub shell_pid: u32,
438    /// Script name or command ($0)
439    pub script_name: String,
440    /// Directory stack for pushd/popd
441    pub dir_stack: Vec<String>,
442    /// Command aliases
443    pub aliases: HashMap<String, String>,
444    /// Whether colors are enabled
445    pub colors_enabled: bool,
446    /// Current color scheme
447    pub color_scheme: ColorScheme,
448    /// Positional parameters ($1, $2, $3, ...)
449    pub positional_params: Vec<String>,
450    /// Function definitions
451    pub functions: HashMap<String, Ast>,
452    /// Local variable stack for function scoping
453    pub local_vars: Vec<HashMap<String, String>>,
454    /// Function call depth for local scope management
455    pub function_depth: usize,
456    /// Maximum allowed recursion depth
457    pub max_recursion_depth: usize,
458    /// Flag to indicate if we're currently returning from a function
459    pub returning: bool,
460    /// Return value when returning from a function
461    pub return_value: Option<i32>,
462    /// Output capture buffer for command substitution
463    pub capture_output: Option<Rc<RefCell<Vec<u8>>>>,
464    /// Whether to use condensed cwd display in prompt
465    pub condensed_cwd: bool,
466    /// Signal trap handlers: maps signal name to command string
467    pub trap_handlers: Arc<Mutex<HashMap<String, String>>>,
468    /// Flag to track if EXIT trap has been executed
469    pub exit_trap_executed: bool,
470    /// Flag to indicate that the shell should exit
471    pub exit_requested: bool,
472    /// Exit code to use when exiting
473    pub exit_code: i32,
474    /// Flag to indicate pending signals need processing
475    /// Set by signal handler, checked by executor
476    #[allow(dead_code)]
477    pub pending_signals: bool,
478    /// Pending here-document content from script execution
479    pub pending_heredoc_content: Option<String>,
480    /// Interactive mode heredoc collection state
481    pub collecting_heredoc: Option<(String, String, String)>, // (command_line, delimiter, collected_content)
482    /// File descriptor table for managing open file descriptors
483    pub fd_table: Rc<RefCell<FileDescriptorTable>>,
484    /// Current subshell nesting depth (for recursion limit)
485    pub subshell_depth: usize,
486    /// Override for stdin (used for pipeline subshells to avoid process-global fd manipulation)
487    pub stdin_override: Option<RawFd>,
488}
489
490impl ShellState {
491    pub fn new() -> Self {
492        let shell_pid = std::process::id();
493
494        // Check NO_COLOR environment variable (respects standard)
495        let no_color = env::var("NO_COLOR").is_ok();
496
497        // Check RUSH_COLORS environment variable for explicit control
498        let rush_colors = env::var("RUSH_COLORS")
499            .map(|v| v.to_lowercase())
500            .unwrap_or_else(|_| "auto".to_string());
501
502        let colors_enabled = match rush_colors.as_str() {
503            "1" | "true" | "on" | "enable" => !no_color && std::io::stdout().is_terminal(),
504            "0" | "false" | "off" | "disable" => false,
505            "auto" => !no_color && std::io::stdout().is_terminal(),
506            _ => !no_color && std::io::stdout().is_terminal(),
507        };
508
509        // Check RUSH_CONDENSED environment variable for cwd display preference
510        let rush_condensed = env::var("RUSH_CONDENSED")
511            .map(|v| v.to_lowercase())
512            .unwrap_or_else(|_| "true".to_string());
513
514        let condensed_cwd = match rush_condensed.as_str() {
515            "1" | "true" | "on" | "enable" => true,
516            "0" | "false" | "off" | "disable" => false,
517            _ => true, // Default to condensed for backward compatibility
518        };
519
520        Self {
521            variables: HashMap::new(),
522            exported: HashSet::new(),
523            last_exit_code: 0,
524            shell_pid,
525            script_name: "rush".to_string(),
526            dir_stack: Vec::new(),
527            aliases: HashMap::new(),
528            colors_enabled,
529            color_scheme: ColorScheme::default(),
530            positional_params: Vec::new(),
531            functions: HashMap::new(),
532            local_vars: Vec::new(),
533            function_depth: 0,
534            max_recursion_depth: 500, // Default recursion limit (reduced to avoid Rust stack overflow)
535            returning: false,
536            return_value: None,
537            capture_output: None,
538            condensed_cwd,
539            trap_handlers: Arc::new(Mutex::new(HashMap::new())),
540            exit_trap_executed: false,
541            exit_requested: false,
542            exit_code: 0,
543            pending_signals: false,
544            pending_heredoc_content: None,
545            collecting_heredoc: None,
546            fd_table: Rc::new(RefCell::new(FileDescriptorTable::new())),
547            subshell_depth: 0,
548            stdin_override: None,
549        }
550    }
551
552    /// Get a variable value, checking local scopes first, then shell variables, then environment
553    pub fn get_var(&self, name: &str) -> Option<String> {
554        // Handle special variables (these are never local)
555        match name {
556            "?" => Some(self.last_exit_code.to_string()),
557            "$" => Some(self.shell_pid.to_string()),
558            "0" => Some(self.script_name.clone()),
559            "*" => {
560                // $* - all positional parameters as single string (space-separated)
561                if self.positional_params.is_empty() {
562                    Some("".to_string())
563                } else {
564                    Some(self.positional_params.join(" "))
565                }
566            }
567            "@" => {
568                // $@ - all positional parameters as separate words (but returns as single string for compatibility)
569                if self.positional_params.is_empty() {
570                    Some("".to_string())
571                } else {
572                    Some(self.positional_params.join(" "))
573                }
574            }
575            "#" => Some(self.positional_params.len().to_string()),
576            _ => {
577                // Handle positional parameters $1, $2, $3, etc. (these are never local)
578                if let Ok(index) = name.parse::<usize>()
579                    && index > 0
580                    && index <= self.positional_params.len()
581                {
582                    return Some(self.positional_params[index - 1].clone());
583                }
584
585                // Check local scopes first, then shell variables, then environment
586                // Search local scopes from innermost to outermost
587                for scope in self.local_vars.iter().rev() {
588                    if let Some(value) = scope.get(name) {
589                        return Some(value.clone());
590                    }
591                }
592
593                // Check shell variables
594                if let Some(value) = self.variables.get(name) {
595                    Some(value.clone())
596                } else {
597                    // Fall back to environment variables
598                    env::var(name).ok()
599                }
600            }
601        }
602    }
603
604    /// Set a shell variable (updates local scope if variable exists there, otherwise sets globally)
605    pub fn set_var(&mut self, name: &str, value: String) {
606        // Check if this variable exists in any local scope
607        // If it does, update it there instead of setting globally
608        for scope in self.local_vars.iter_mut().rev() {
609            if scope.contains_key(name) {
610                scope.insert(name.to_string(), value);
611                return;
612            }
613        }
614
615        // Variable doesn't exist in local scopes, set it globally
616        self.variables.insert(name.to_string(), value);
617    }
618
619    /// Remove a shell variable
620    pub fn unset_var(&mut self, name: &str) {
621        self.variables.remove(name);
622        self.exported.remove(name);
623    }
624
625    /// Mark a variable as exported
626    pub fn export_var(&mut self, name: &str) {
627        if self.variables.contains_key(name) {
628            self.exported.insert(name.to_string());
629        }
630    }
631
632    /// Set and export a variable
633    pub fn set_exported_var(&mut self, name: &str, value: String) {
634        self.set_var(name, value);
635        self.export_var(name);
636    }
637
638    /// Get all environment variables for child processes (exported + inherited)
639    pub fn get_env_for_child(&self) -> HashMap<String, String> {
640        let mut child_env = HashMap::new();
641
642        // Add all current environment variables
643        for (key, value) in env::vars() {
644            child_env.insert(key, value);
645        }
646
647        // Override with exported shell variables
648        for var_name in &self.exported {
649            if let Some(value) = self.variables.get(var_name) {
650                child_env.insert(var_name.clone(), value.clone());
651            }
652        }
653
654        child_env
655    }
656
657    /// Update the last exit code
658    pub fn set_last_exit_code(&mut self, code: i32) {
659        self.last_exit_code = code;
660    }
661
662    /// Set the script name ($0)
663    pub fn set_script_name(&mut self, name: &str) {
664        self.script_name = name.to_string();
665    }
666
667    /// Get the condensed current working directory for the prompt
668    pub fn get_condensed_cwd(&self) -> String {
669        match env::current_dir() {
670            Ok(path) => {
671                let path_str = path.to_string_lossy();
672                let components: Vec<&str> = path_str.split('/').collect();
673                if components.is_empty() || (components.len() == 1 && components[0].is_empty()) {
674                    return "/".to_string();
675                }
676                let mut result = String::new();
677                for (i, comp) in components.iter().enumerate() {
678                    if comp.is_empty() {
679                        continue; // skip leading empty component
680                    }
681                    if i == components.len() - 1 {
682                        result.push('/');
683                        result.push_str(comp);
684                    } else {
685                        result.push('/');
686                        if let Some(first) = comp.chars().next() {
687                            result.push(first);
688                        }
689                    }
690                }
691                if result.is_empty() {
692                    "/".to_string()
693                } else {
694                    result
695                }
696            }
697            Err(_) => "/?".to_string(), // fallback if can't get cwd
698        }
699    }
700
701    /// Get the full current working directory for the prompt
702    pub fn get_full_cwd(&self) -> String {
703        match env::current_dir() {
704            Ok(path) => path.to_string_lossy().to_string(),
705            Err(_) => "/?".to_string(), // fallback if can't get cwd
706        }
707    }
708
709    /// Get the user@hostname string for the prompt
710    pub fn get_user_hostname(&self) -> String {
711        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
712
713        // First try to get hostname from HOSTNAME environment variable
714        if let Ok(hostname) = env::var("HOSTNAME")
715            && !hostname.trim().is_empty()
716        {
717            return format!("{}@{}", user, hostname);
718        }
719
720        // If HOSTNAME is not set or empty, try the hostname command
721        let hostname = match std::process::Command::new("hostname").output() {
722            Ok(output) if output.status.success() => {
723                String::from_utf8_lossy(&output.stdout).trim().to_string()
724            }
725            _ => "hostname".to_string(), // Last resort fallback
726        };
727
728        // Set the HOSTNAME environment variable for future use
729        if hostname != "hostname" {
730            unsafe {
731                env::set_var("HOSTNAME", &hostname);
732            }
733        }
734
735        format!("{}@{}", user, hostname)
736    }
737
738    /// Get the full prompt string
739    pub fn get_prompt(&self) -> String {
740        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
741        let prompt_char = if user == "root" { "#" } else { "$" };
742        let cwd = if self.condensed_cwd {
743            self.get_condensed_cwd()
744        } else {
745            self.get_full_cwd()
746        };
747        format!("{}:{} {} ", self.get_user_hostname(), cwd, prompt_char)
748    }
749
750    /// Set an alias
751    pub fn set_alias(&mut self, name: &str, value: String) {
752        self.aliases.insert(name.to_string(), value);
753    }
754
755    /// Get an alias value
756    pub fn get_alias(&self, name: &str) -> Option<&String> {
757        self.aliases.get(name)
758    }
759
760    /// Remove an alias
761    pub fn remove_alias(&mut self, name: &str) {
762        self.aliases.remove(name);
763    }
764
765    /// Get all aliases
766    pub fn get_all_aliases(&self) -> &HashMap<String, String> {
767        &self.aliases
768    }
769
770    /// Set positional parameters
771    pub fn set_positional_params(&mut self, params: Vec<String>) {
772        self.positional_params = params;
773    }
774
775    /// Get positional parameters
776    #[allow(dead_code)]
777    pub fn get_positional_params(&self) -> &[String] {
778        &self.positional_params
779    }
780
781    /// Shift positional parameters (remove first n parameters)
782    pub fn shift_positional_params(&mut self, count: usize) {
783        if count > 0 {
784            for _ in 0..count {
785                if !self.positional_params.is_empty() {
786                    self.positional_params.remove(0);
787                }
788            }
789        }
790    }
791
792    /// Add a positional parameter at the end
793    #[allow(dead_code)]
794    pub fn push_positional_param(&mut self, param: String) {
795        self.positional_params.push(param);
796    }
797
798    /// Define a function
799    pub fn define_function(&mut self, name: String, body: Ast) {
800        self.functions.insert(name, body);
801    }
802
803    /// Get a function definition
804    pub fn get_function(&self, name: &str) -> Option<&Ast> {
805        self.functions.get(name)
806    }
807
808    /// Remove a function definition
809    #[allow(dead_code)]
810    pub fn remove_function(&mut self, name: &str) {
811        self.functions.remove(name);
812    }
813
814    /// Get all function names
815    #[allow(dead_code)]
816    pub fn get_function_names(&self) -> Vec<&String> {
817        self.functions.keys().collect()
818    }
819
820    /// Push a new local variable scope
821    pub fn push_local_scope(&mut self) {
822        self.local_vars.push(HashMap::new());
823    }
824
825    /// Pop the current local variable scope
826    pub fn pop_local_scope(&mut self) {
827        if !self.local_vars.is_empty() {
828            self.local_vars.pop();
829        }
830    }
831
832    /// Set a local variable in the current scope
833    pub fn set_local_var(&mut self, name: &str, value: String) {
834        if let Some(current_scope) = self.local_vars.last_mut() {
835            current_scope.insert(name.to_string(), value);
836        } else {
837            // If no local scope exists, set as global variable
838            self.set_var(name, value);
839        }
840    }
841
842    /// Enter a function context (push local scope if needed)
843    pub fn enter_function(&mut self) {
844        self.function_depth += 1;
845        if self.function_depth > self.local_vars.len() {
846            self.push_local_scope();
847        }
848    }
849
850    /// Exit a function context (pop local scope if needed)
851    pub fn exit_function(&mut self) {
852        if self.function_depth > 0 {
853            self.function_depth -= 1;
854            if self.function_depth == self.local_vars.len() - 1 {
855                self.pop_local_scope();
856            }
857        }
858    }
859
860    /// Set return state for function returns
861    pub fn set_return(&mut self, value: i32) {
862        self.returning = true;
863        self.return_value = Some(value);
864    }
865
866    /// Clear return state
867    pub fn clear_return(&mut self) {
868        self.returning = false;
869        self.return_value = None;
870    }
871
872    /// Check if currently returning
873    pub fn is_returning(&self) -> bool {
874        self.returning
875    }
876
877    /// Get return value if returning
878    pub fn get_return_value(&self) -> Option<i32> {
879        self.return_value
880    }
881
882    /// Set a trap handler for a signal
883    pub fn set_trap(&mut self, signal: &str, command: String) {
884        if let Ok(mut handlers) = self.trap_handlers.lock() {
885            handlers.insert(signal.to_uppercase(), command);
886        }
887    }
888
889    /// Get a trap handler for a signal
890    pub fn get_trap(&self, signal: &str) -> Option<String> {
891        if let Ok(handlers) = self.trap_handlers.lock() {
892            handlers.get(&signal.to_uppercase()).cloned()
893        } else {
894            None
895        }
896    }
897
898    /// Remove a trap handler for a signal
899    pub fn remove_trap(&mut self, signal: &str) {
900        if let Ok(mut handlers) = self.trap_handlers.lock() {
901            handlers.remove(&signal.to_uppercase());
902        }
903    }
904
905    /// Get all trap handlers
906    pub fn get_all_traps(&self) -> HashMap<String, String> {
907        if let Ok(handlers) = self.trap_handlers.lock() {
908            handlers.clone()
909        } else {
910            HashMap::new()
911        }
912    }
913
914    /// Clear all trap handlers
915    #[allow(dead_code)]
916    pub fn clear_traps(&mut self) {
917        if let Ok(mut handlers) = self.trap_handlers.lock() {
918            handlers.clear();
919        }
920    }
921}
922
923/// Enqueue a signal event for later processing
924/// If the queue is full, the oldest event is dropped
925pub fn enqueue_signal(signal_name: &str, signal_number: i32) {
926    if let Ok(mut queue) = SIGNAL_QUEUE.lock() {
927        // If queue is full, remove oldest event
928        if queue.len() >= MAX_SIGNAL_QUEUE_SIZE {
929            queue.pop_front();
930            eprintln!("Warning: Signal queue overflow, dropping oldest signal");
931        }
932
933        queue.push_back(SignalEvent::new(signal_name.to_string(), signal_number));
934    }
935}
936
937/// Process all pending signals in the queue
938/// This should be called at safe points during command execution
939pub fn process_pending_signals(shell_state: &mut ShellState) {
940    // Try to lock the queue with a timeout to avoid blocking
941    if let Ok(mut queue) = SIGNAL_QUEUE.lock() {
942        // Process all pending signals
943        while let Some(signal_event) = queue.pop_front() {
944            // Check if a trap is set for this signal
945            if let Some(trap_cmd) = shell_state.get_trap(&signal_event.signal_name)
946                && !trap_cmd.is_empty()
947            {
948                // Display signal information for debugging/tracking
949                if shell_state.colors_enabled {
950                    eprintln!(
951                        "{}Signal {} (signal {}) received at {:?}\x1b[0m",
952                        shell_state.color_scheme.builtin,
953                        signal_event.signal_name,
954                        signal_event.signal_number,
955                        signal_event.timestamp
956                    );
957                } else {
958                    eprintln!(
959                        "Signal {} (signal {}) received at {:?}",
960                        signal_event.signal_name,
961                        signal_event.signal_number,
962                        signal_event.timestamp
963                    );
964                }
965
966                // Execute the trap handler
967                // Note: This preserves the exit code as per POSIX requirements
968                crate::executor::execute_trap_handler(&trap_cmd, shell_state);
969            }
970        }
971    }
972}
973
974impl Default for ShellState {
975    fn default() -> Self {
976        Self::new()
977    }
978}
979
980#[cfg(test)]
981mod tests {
982    use super::*;
983    use std::sync::Mutex;
984
985    // Mutex to serialize tests that create temporary files
986    static FILE_LOCK: Mutex<()> = Mutex::new(());
987
988    #[test]
989    fn test_shell_state_basic() {
990        let mut state = ShellState::new();
991        state.set_var("TEST_VAR", "test_value".to_string());
992        assert_eq!(state.get_var("TEST_VAR"), Some("test_value".to_string()));
993    }
994
995    #[test]
996    fn test_special_variables() {
997        let mut state = ShellState::new();
998        state.set_last_exit_code(42);
999        state.set_script_name("test_script");
1000
1001        assert_eq!(state.get_var("?"), Some("42".to_string()));
1002        assert_eq!(state.get_var("$"), Some(state.shell_pid.to_string()));
1003        assert_eq!(state.get_var("0"), Some("test_script".to_string()));
1004    }
1005
1006    #[test]
1007    fn test_export_variable() {
1008        let mut state = ShellState::new();
1009        state.set_var("EXPORT_VAR", "export_value".to_string());
1010        state.export_var("EXPORT_VAR");
1011
1012        let child_env = state.get_env_for_child();
1013        assert_eq!(
1014            child_env.get("EXPORT_VAR"),
1015            Some(&"export_value".to_string())
1016        );
1017    }
1018
1019    #[test]
1020    fn test_unset_variable() {
1021        let mut state = ShellState::new();
1022        state.set_var("UNSET_VAR", "value".to_string());
1023        state.export_var("UNSET_VAR");
1024
1025        assert!(state.variables.contains_key("UNSET_VAR"));
1026        assert!(state.exported.contains("UNSET_VAR"));
1027
1028        state.unset_var("UNSET_VAR");
1029
1030        assert!(!state.variables.contains_key("UNSET_VAR"));
1031        assert!(!state.exported.contains("UNSET_VAR"));
1032    }
1033
1034    #[test]
1035    fn test_get_user_hostname() {
1036        let state = ShellState::new();
1037        let user_hostname = state.get_user_hostname();
1038        // Should contain @ since it's user@hostname format
1039        assert!(user_hostname.contains('@'));
1040    }
1041
1042    #[test]
1043    fn test_get_prompt() {
1044        let state = ShellState::new();
1045        let prompt = state.get_prompt();
1046        // Should end with $ and contain @
1047        assert!(prompt.ends_with(" $ "));
1048        assert!(prompt.contains('@'));
1049    }
1050
1051    #[test]
1052    fn test_positional_parameters() {
1053        let mut state = ShellState::new();
1054        state.set_positional_params(vec![
1055            "arg1".to_string(),
1056            "arg2".to_string(),
1057            "arg3".to_string(),
1058        ]);
1059
1060        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
1061        assert_eq!(state.get_var("2"), Some("arg2".to_string()));
1062        assert_eq!(state.get_var("3"), Some("arg3".to_string()));
1063        assert_eq!(state.get_var("4"), None);
1064        assert_eq!(state.get_var("#"), Some("3".to_string()));
1065        assert_eq!(state.get_var("*"), Some("arg1 arg2 arg3".to_string()));
1066        assert_eq!(state.get_var("@"), Some("arg1 arg2 arg3".to_string()));
1067    }
1068
1069    #[test]
1070    fn test_positional_parameters_empty() {
1071        let mut state = ShellState::new();
1072        state.set_positional_params(vec![]);
1073
1074        assert_eq!(state.get_var("1"), None);
1075        assert_eq!(state.get_var("#"), Some("0".to_string()));
1076        assert_eq!(state.get_var("*"), Some("".to_string()));
1077        assert_eq!(state.get_var("@"), Some("".to_string()));
1078    }
1079
1080    #[test]
1081    fn test_shift_positional_params() {
1082        let mut state = ShellState::new();
1083        state.set_positional_params(vec![
1084            "arg1".to_string(),
1085            "arg2".to_string(),
1086            "arg3".to_string(),
1087        ]);
1088
1089        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
1090        assert_eq!(state.get_var("2"), Some("arg2".to_string()));
1091        assert_eq!(state.get_var("3"), Some("arg3".to_string()));
1092
1093        state.shift_positional_params(1);
1094
1095        assert_eq!(state.get_var("1"), Some("arg2".to_string()));
1096        assert_eq!(state.get_var("2"), Some("arg3".to_string()));
1097        assert_eq!(state.get_var("3"), None);
1098        assert_eq!(state.get_var("#"), Some("2".to_string()));
1099
1100        state.shift_positional_params(2);
1101
1102        assert_eq!(state.get_var("1"), None);
1103        assert_eq!(state.get_var("#"), Some("0".to_string()));
1104    }
1105
1106    #[test]
1107    fn test_push_positional_param() {
1108        let mut state = ShellState::new();
1109        state.set_positional_params(vec!["arg1".to_string()]);
1110
1111        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
1112        assert_eq!(state.get_var("#"), Some("1".to_string()));
1113
1114        state.push_positional_param("arg2".to_string());
1115
1116        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
1117        assert_eq!(state.get_var("2"), Some("arg2".to_string()));
1118        assert_eq!(state.get_var("#"), Some("2".to_string()));
1119    }
1120
1121    #[test]
1122    fn test_local_variable_scoping() {
1123        let mut state = ShellState::new();
1124
1125        // Set a global variable
1126        state.set_var("global_var", "global_value".to_string());
1127        assert_eq!(
1128            state.get_var("global_var"),
1129            Some("global_value".to_string())
1130        );
1131
1132        // Push local scope
1133        state.push_local_scope();
1134
1135        // Set a local variable with the same name
1136        state.set_local_var("global_var", "local_value".to_string());
1137        assert_eq!(state.get_var("global_var"), Some("local_value".to_string()));
1138
1139        // Set another local variable
1140        state.set_local_var("local_var", "local_only".to_string());
1141        assert_eq!(state.get_var("local_var"), Some("local_only".to_string()));
1142
1143        // Pop local scope
1144        state.pop_local_scope();
1145
1146        // Should be back to global variable
1147        assert_eq!(
1148            state.get_var("global_var"),
1149            Some("global_value".to_string())
1150        );
1151        assert_eq!(state.get_var("local_var"), None);
1152    }
1153
1154    #[test]
1155    fn test_nested_local_scopes() {
1156        let mut state = ShellState::new();
1157
1158        // Set global variable
1159        state.set_var("test_var", "global".to_string());
1160
1161        // Push first local scope
1162        state.push_local_scope();
1163        state.set_local_var("test_var", "level1".to_string());
1164        assert_eq!(state.get_var("test_var"), Some("level1".to_string()));
1165
1166        // Push second local scope
1167        state.push_local_scope();
1168        state.set_local_var("test_var", "level2".to_string());
1169        assert_eq!(state.get_var("test_var"), Some("level2".to_string()));
1170
1171        // Pop second scope
1172        state.pop_local_scope();
1173        assert_eq!(state.get_var("test_var"), Some("level1".to_string()));
1174
1175        // Pop first scope
1176        state.pop_local_scope();
1177        assert_eq!(state.get_var("test_var"), Some("global".to_string()));
1178    }
1179
1180    #[test]
1181    fn test_variable_set_in_local_scope() {
1182        let mut state = ShellState::new();
1183
1184        // No local scope initially
1185        state.set_var("test_var", "global".to_string());
1186        assert_eq!(state.get_var("test_var"), Some("global".to_string()));
1187
1188        // Push local scope and set local variable
1189        state.push_local_scope();
1190        state.set_local_var("test_var", "local".to_string());
1191        assert_eq!(state.get_var("test_var"), Some("local".to_string()));
1192
1193        // Pop scope
1194        state.pop_local_scope();
1195        assert_eq!(state.get_var("test_var"), Some("global".to_string()));
1196    }
1197
1198    #[test]
1199    fn test_condensed_cwd_environment_variable() {
1200        // Test default behavior (should be true for backward compatibility)
1201        let state = ShellState::new();
1202        assert!(state.condensed_cwd);
1203
1204        // Test explicit true
1205        unsafe {
1206            env::set_var("RUSH_CONDENSED", "true");
1207        }
1208        let state = ShellState::new();
1209        assert!(state.condensed_cwd);
1210
1211        // Test explicit false
1212        unsafe {
1213            env::set_var("RUSH_CONDENSED", "false");
1214        }
1215        let state = ShellState::new();
1216        assert!(!state.condensed_cwd);
1217
1218        // Clean up
1219        unsafe {
1220            env::remove_var("RUSH_CONDENSED");
1221        }
1222    }
1223
1224    #[test]
1225    fn test_get_full_cwd() {
1226        let state = ShellState::new();
1227        let full_cwd = state.get_full_cwd();
1228        assert!(!full_cwd.is_empty());
1229        // Should contain path separators (either / or \ depending on platform)
1230        assert!(full_cwd.contains('/') || full_cwd.contains('\\'));
1231    }
1232
1233    #[test]
1234    fn test_prompt_with_condensed_setting() {
1235        let mut state = ShellState::new();
1236
1237        // Test with condensed enabled (default)
1238        assert!(state.condensed_cwd);
1239        let prompt_condensed = state.get_prompt();
1240        assert!(prompt_condensed.contains('@'));
1241
1242        // Test with condensed disabled
1243        state.condensed_cwd = false;
1244        let prompt_full = state.get_prompt();
1245        assert!(prompt_full.contains('@'));
1246
1247        // Both should end with "$ " (or "# " for root)
1248        assert!(prompt_condensed.ends_with("$ ") || prompt_condensed.ends_with("# "));
1249        assert!(prompt_full.ends_with("$ ") || prompt_full.ends_with("# "));
1250    }
1251
1252    // File Descriptor Table Tests
1253
1254    #[test]
1255    fn test_fd_table_creation() {
1256        let fd_table = FileDescriptorTable::new();
1257        assert!(!fd_table.is_open(0));
1258        assert!(!fd_table.is_open(1));
1259        assert!(!fd_table.is_open(2));
1260    }
1261
1262    #[test]
1263    fn test_fd_table_open_file() {
1264        let mut fd_table = FileDescriptorTable::new();
1265
1266        // Create a temporary file
1267        let temp_file = "/tmp/rush_test_fd_open.txt";
1268        std::fs::write(temp_file, "test content").unwrap();
1269
1270        // Open file for reading
1271        let result = fd_table.open_fd(3, temp_file, true, false, false, false);
1272        assert!(result.is_ok());
1273        assert!(fd_table.is_open(3));
1274
1275        // Clean up
1276        let _ = std::fs::remove_file(temp_file);
1277    }
1278
1279    #[test]
1280    fn test_fd_table_open_file_for_writing() {
1281        let mut fd_table = FileDescriptorTable::new();
1282
1283        // Create a temporary file path
1284        let temp_file = "/tmp/rush_test_fd_write.txt";
1285
1286        // Open file for writing
1287        let result = fd_table.open_fd(4, temp_file, false, true, false, true);
1288        assert!(result.is_ok());
1289        assert!(fd_table.is_open(4));
1290
1291        // Clean up
1292        let _ = std::fs::remove_file(temp_file);
1293    }
1294
1295    #[test]
1296    fn test_fd_table_invalid_fd_number() {
1297        let mut fd_table = FileDescriptorTable::new();
1298
1299        // Test invalid fd numbers
1300        let result = fd_table.open_fd(-1, "/tmp/test.txt", true, false, false, false);
1301        assert!(result.is_err());
1302        assert!(result.unwrap_err().contains("Invalid file descriptor"));
1303
1304        let result = fd_table.open_fd(1025, "/tmp/test.txt", true, false, false, false);
1305        assert!(result.is_err());
1306        assert!(result.unwrap_err().contains("Invalid file descriptor"));
1307    }
1308
1309    #[test]
1310    fn test_fd_table_duplicate_fd() {
1311        let mut fd_table = FileDescriptorTable::new();
1312
1313        // Create a temporary file
1314        let temp_file = "/tmp/rush_test_fd_dup.txt";
1315        std::fs::write(temp_file, "test content").unwrap();
1316
1317        // Open file on fd 3
1318        fd_table
1319            .open_fd(3, temp_file, true, false, false, false)
1320            .unwrap();
1321        assert!(fd_table.is_open(3));
1322
1323        // Duplicate fd 3 to fd 4
1324        let result = fd_table.duplicate_fd(3, 4);
1325        assert!(result.is_ok());
1326        assert!(fd_table.is_open(4));
1327
1328        // Clean up
1329        let _ = std::fs::remove_file(temp_file);
1330    }
1331
1332    #[test]
1333    fn test_fd_table_duplicate_to_self() {
1334        let mut fd_table = FileDescriptorTable::new();
1335
1336        // Create a temporary file
1337        let temp_file = "/tmp/rush_test_fd_dup_self.txt";
1338        std::fs::write(temp_file, "test content").unwrap();
1339
1340        // Open file on fd 3
1341        fd_table
1342            .open_fd(3, temp_file, true, false, false, false)
1343            .unwrap();
1344
1345        // Duplicate fd 3 to itself (should be no-op)
1346        let result = fd_table.duplicate_fd(3, 3);
1347        assert!(result.is_ok());
1348        assert!(fd_table.is_open(3));
1349
1350        // Clean up
1351        let _ = std::fs::remove_file(temp_file);
1352    }
1353
1354    #[test]
1355    fn test_fd_table_duplicate_closed_fd() {
1356        let mut fd_table = FileDescriptorTable::new();
1357
1358        // Try to duplicate a closed fd
1359        let result = fd_table.duplicate_fd(3, 4);
1360        assert!(result.is_err());
1361        assert!(result.unwrap_err().contains("not open"));
1362    }
1363
1364    #[test]
1365    fn test_fd_table_close_fd() {
1366        let mut fd_table = FileDescriptorTable::new();
1367
1368        // Create a temporary file
1369        let temp_file = "/tmp/rush_test_fd_close.txt";
1370        std::fs::write(temp_file, "test content").unwrap();
1371
1372        // Open file on fd 3
1373        fd_table
1374            .open_fd(3, temp_file, true, false, false, false)
1375            .unwrap();
1376        assert!(fd_table.is_open(3));
1377
1378        // Close fd 3
1379        let result = fd_table.close_fd(3);
1380        assert!(result.is_ok());
1381        assert!(fd_table.is_closed(3));
1382        assert!(!fd_table.is_open(3));
1383
1384        // Clean up
1385        let _ = std::fs::remove_file(temp_file);
1386    }
1387
1388    #[test]
1389    fn test_fd_table_save_and_restore() {
1390        let mut fd_table = FileDescriptorTable::new();
1391
1392        // Save stdin (fd 0)
1393        let result = fd_table.save_fd(0);
1394        assert!(result.is_ok());
1395
1396        // Restore stdin
1397        let result = fd_table.restore_fd(0);
1398        assert!(result.is_ok());
1399    }
1400
1401    #[test]
1402    fn test_fd_table_save_all_and_restore_all() {
1403        let _lock = FILE_LOCK.lock().unwrap();
1404        let mut fd_table = FileDescriptorTable::new();
1405
1406        // Create unique temporary files
1407        use std::time::{SystemTime, UNIX_EPOCH};
1408        let timestamp = SystemTime::now()
1409            .duration_since(UNIX_EPOCH)
1410            .unwrap()
1411            .as_nanos();
1412        let temp_file1 = format!("/tmp/rush_test_fd_save1_{}.txt", timestamp);
1413        let temp_file2 = format!("/tmp/rush_test_fd_save2_{}.txt", timestamp);
1414
1415        std::fs::write(&temp_file1, "test content 1").unwrap();
1416        std::fs::write(&temp_file2, "test content 2").unwrap();
1417
1418        // Open files on fd 50 and 51
1419        // Manually dup2 to ensure these FDs are valid for save_fd()
1420        // Using higher FDs to avoid conflict with parallel tests using 0-9
1421        let f1 = File::open(&temp_file1).unwrap();
1422        let f2 = File::open(&temp_file2).unwrap();
1423        unsafe {
1424            libc::dup2(f1.as_raw_fd(), 50);
1425            libc::dup2(f2.as_raw_fd(), 51);
1426        }
1427
1428        fd_table
1429            .open_fd(50, &temp_file1, true, false, false, false)
1430            .unwrap();
1431        fd_table
1432            .open_fd(51, &temp_file2, true, false, false, false)
1433            .unwrap();
1434
1435        // Save all fds
1436        let result = fd_table.save_all_fds();
1437        assert!(result.is_ok());
1438
1439        // Restore all fds
1440        let result = fd_table.restore_all_fds();
1441        assert!(result.is_ok());
1442
1443        // Clean up
1444        unsafe {
1445            libc::close(50);
1446            libc::close(51);
1447        }
1448        let _ = std::fs::remove_file(&temp_file1);
1449        let _ = std::fs::remove_file(&temp_file2);
1450    }
1451
1452    #[test]
1453    fn test_fd_table_clear() {
1454        let mut fd_table = FileDescriptorTable::new();
1455
1456        // Create a temporary file
1457        let temp_file = "/tmp/rush_test_fd_clear.txt";
1458        std::fs::write(temp_file, "test content").unwrap();
1459
1460        // Open file on fd 50 (was 3)
1461        // Manual setup not strictly needed for clear() test as it checks map?
1462        // But clear() might close FDs?
1463        // FileDescriptorTable::clear() just clears map. File drops.
1464
1465        fd_table
1466            .open_fd(50, temp_file, true, false, false, false)
1467            .unwrap();
1468        assert!(fd_table.is_open(50));
1469
1470        // Clear all fds
1471        fd_table.clear();
1472        assert!(!fd_table.is_open(3));
1473
1474        // Clean up
1475        let _ = std::fs::remove_file(temp_file);
1476    }
1477
1478    #[test]
1479    fn test_fd_table_get_stdio() {
1480        let mut fd_table = FileDescriptorTable::new();
1481
1482        // Create a temporary file
1483        let temp_file = "/tmp/rush_test_fd_stdio.txt";
1484        std::fs::write(temp_file, "test content").unwrap();
1485
1486        // Open file on fd 3
1487        fd_table
1488            .open_fd(3, temp_file, true, false, false, false)
1489            .unwrap();
1490
1491        // Get Stdio for fd 3
1492        let stdio = fd_table.get_stdio(3);
1493        assert!(stdio.is_some());
1494
1495        // Get Stdio for non-existent fd
1496        let stdio = fd_table.get_stdio(5);
1497        assert!(stdio.is_none());
1498
1499        // Clean up
1500        let _ = std::fs::remove_file(temp_file);
1501    }
1502
1503    #[test]
1504    fn test_fd_table_multiple_operations() {
1505        let mut fd_table = FileDescriptorTable::new();
1506
1507        // Create temporary files
1508        let temp_file1 = "/tmp/rush_test_fd_multi1.txt";
1509        let temp_file2 = "/tmp/rush_test_fd_multi2.txt";
1510        std::fs::write(temp_file1, "test content 1").unwrap();
1511        std::fs::write(temp_file2, "test content 2").unwrap();
1512
1513        // Open file on fd 3
1514        fd_table
1515            .open_fd(3, temp_file1, true, false, false, false)
1516            .unwrap();
1517        assert!(fd_table.is_open(3));
1518
1519        // Duplicate fd 3 to fd 4
1520        fd_table.duplicate_fd(3, 4).unwrap();
1521        assert!(fd_table.is_open(4));
1522
1523        // Open another file on fd 5
1524        fd_table
1525            .open_fd(5, temp_file2, true, false, false, false)
1526            .unwrap();
1527        assert!(fd_table.is_open(5));
1528
1529        // Close fd 4
1530        fd_table.close_fd(4).unwrap();
1531        assert!(fd_table.is_closed(4));
1532        assert!(!fd_table.is_open(4));
1533
1534        // fd 3 and 5 should still be open
1535        assert!(fd_table.is_open(3));
1536        assert!(fd_table.is_open(5));
1537
1538        // Clean up
1539        let _ = std::fs::remove_file(temp_file1);
1540        let _ = std::fs::remove_file(temp_file2);
1541    }
1542
1543    #[test]
1544    fn test_shell_state_has_fd_table() {
1545        let state = ShellState::new();
1546        let fd_table = state.fd_table.borrow();
1547        assert!(!fd_table.is_open(3));
1548    }
1549
1550    #[test]
1551    fn test_shell_state_fd_table_operations() {
1552        let state = ShellState::new();
1553
1554        // Create a temporary file
1555        let temp_file = "/tmp/rush_test_state_fd.txt";
1556        std::fs::write(temp_file, "test content").unwrap();
1557
1558        // Open file through shell state's fd table
1559        {
1560            let mut fd_table = state.fd_table.borrow_mut();
1561            fd_table
1562                .open_fd(3, temp_file, true, false, false, false)
1563                .unwrap();
1564        }
1565
1566        // Verify it's open
1567        {
1568            let fd_table = state.fd_table.borrow();
1569            assert!(fd_table.is_open(3));
1570        }
1571
1572        // Clean up
1573        let _ = std::fs::remove_file(temp_file);
1574    }
1575}