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