rush_sh/
state.rs

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