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