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