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        create_new: bool,
111    ) -> Result<(), String> {
112        let mut opts = OpenOptions::new();
113        if create_new {
114            opts.create_new(true); // Atomic check-and-create
115        } else if truncate {
116            opts.create(true).truncate(true);
117        }
118
119        // Validate fd number
120        if !(0..=1024).contains(&fd_num) {
121            return Err(format!("Invalid file descriptor number: {}", fd_num));
122        }
123
124        // Open the file with the specified options
125        let file = OpenOptions::new()
126            .read(read)
127            .write(write)
128            .append(append)
129            .truncate(truncate)
130            .create(write || append)
131            .open(path)
132            .map_err(|e| format!("Cannot open {}: {}", path, e))?;
133
134        // Store the file descriptor
135        self.fds.insert(fd_num, FileDescriptor::File(file));
136        Ok(())
137    }
138
139    /// Duplicate a file descriptor
140    ///
141    /// # Arguments
142    /// * `source_fd` - The source file descriptor to duplicate
143    /// * `target_fd` - The target file descriptor number
144    ///
145    /// # Returns
146    /// * `Ok(())` on success
147    /// * `Err(String)` with error message on failure
148    pub fn duplicate_fd(&mut self, source_fd: i32, target_fd: i32) -> Result<(), String> {
149        // Validate fd numbers
150        if !(0..=1024).contains(&source_fd) {
151            return Err(format!("Invalid source file descriptor: {}", source_fd));
152        }
153        if !(0..=1024).contains(&target_fd) {
154            return Err(format!("Invalid target file descriptor: {}", target_fd));
155        }
156
157        // POSIX: Duplicating to self is a no-op
158        if source_fd == target_fd {
159            return Ok(());
160        }
161
162        // Get the raw fd to duplicate
163        let raw_fd = match self.get_raw_fd(source_fd) {
164            Some(fd) => fd,
165            None => {
166                return Err(format!(
167                    "File descriptor {} is not open or is closed",
168                    source_fd
169                ));
170            }
171        };
172
173        // Store the duplication
174        self.fds
175            .insert(target_fd, FileDescriptor::Duplicate(raw_fd));
176        Ok(())
177    }
178
179    /// Close a file descriptor
180    ///
181    /// # Arguments
182    /// * `fd_num` - The file descriptor number to close
183    ///
184    /// # Returns
185    /// * `Ok(())` on success
186    /// * `Err(String)` with error message on failure
187    pub fn close_fd(&mut self, fd_num: i32) -> Result<(), String> {
188        // Validate fd number
189        if !(0..=1024).contains(&fd_num) {
190            return Err(format!("Invalid file descriptor number: {}", fd_num));
191        }
192
193        // Mark the fd as closed
194        self.fds.insert(fd_num, FileDescriptor::Closed);
195        Ok(())
196    }
197
198    /// Save the current state of a file descriptor for later restoration
199    ///
200    /// # Arguments
201    /// * `fd_num` - The file descriptor number to save
202    ///
203    /// # Returns
204    /// * `Ok(())` on success
205    /// * `Err(String)` with error message on failure
206    pub fn save_fd(&mut self, fd_num: i32) -> Result<(), String> {
207        // Validate fd number
208        if !(0..=1024).contains(&fd_num) {
209            return Err(format!("Invalid file descriptor number: {}", fd_num));
210        }
211
212        // Duplicate the fd using dup() syscall to save it
213        let saved_fd = unsafe {
214            let raw_fd = fd_num as RawFd;
215            libc::dup(raw_fd)
216        };
217
218        if saved_fd < 0 {
219            return Err(format!("Failed to save file descriptor {}", fd_num));
220        }
221
222        self.saved_fds.insert(fd_num, saved_fd);
223        Ok(())
224    }
225
226    /// Restore a previously saved file descriptor
227    ///
228    /// # Arguments
229    /// * `fd_num` - The file descriptor number to restore
230    ///
231    /// # Returns
232    /// * `Ok(())` on success
233    /// * `Err(String)` with error message on failure
234    pub fn restore_fd(&mut self, fd_num: i32) -> Result<(), String> {
235        // Validate fd number
236        if !(0..=1024).contains(&fd_num) {
237            return Err(format!("Invalid file descriptor number: {}", fd_num));
238        }
239
240        // Get the saved fd
241        if let Some(saved_fd) = self.saved_fds.remove(&fd_num) {
242            // Restore using dup2() syscall
243            unsafe {
244                let result = libc::dup2(saved_fd, fd_num as RawFd);
245                libc::close(saved_fd); // Close the saved fd
246
247                if result < 0 {
248                    return Err(format!("Failed to restore file descriptor {}", fd_num));
249                }
250            }
251
252            // Remove from our tracking
253            self.fds.remove(&fd_num);
254        }
255
256        Ok(())
257    }
258
259    /// Create a deep copy of the file descriptor table
260    /// This duplicates all open file descriptors so they are independent of the original table
261    pub fn deep_clone(&self) -> Result<Self, String> {
262        let mut new_fds = HashMap::new();
263        for (fd, descriptor) in &self.fds {
264            new_fds.insert(*fd, descriptor.try_clone()?);
265        }
266
267        Ok(Self {
268            fds: new_fds,
269            saved_fds: self.saved_fds.clone(),
270        })
271    }
272
273    /// Save all currently open file descriptors
274    ///
275    /// # Returns
276    /// * `Ok(())` on success
277    /// * `Err(String)` with error message on failure
278    pub fn save_all_fds(&mut self) -> Result<(), String> {
279        // Save all fds that we're tracking
280        let fd_nums: Vec<i32> = self.fds.keys().copied().collect();
281        for fd_num in fd_nums {
282            self.save_fd(fd_num)?;
283        }
284
285        // Also explicitly save standard FDs (0, 1, 2) if they aren't already tracked
286        // This ensures changes to standard streams (via CommandGroup etc.) can be restored
287        for fd in 0..=2 {
288            if !self.fds.contains_key(&fd) {
289                // Try to save, ignore error if fd is closed/invalid
290                let _ = self.save_fd(fd);
291            }
292        }
293        Ok(())
294    }
295
296    /// Restore all previously saved file descriptors
297    ///
298    /// # Returns
299    /// * `Ok(())` on success
300    /// * `Err(String)` with error message on failure
301    pub fn restore_all_fds(&mut self) -> Result<(), String> {
302        // Restore all saved fds
303        let fd_nums: Vec<i32> = self.saved_fds.keys().copied().collect();
304        for fd_num in fd_nums {
305            self.restore_fd(fd_num)?;
306        }
307        Ok(())
308    }
309
310    /// Get a file handle for a given file descriptor number
311    ///
312    /// # Arguments
313    /// * `fd_num` - The file descriptor number
314    ///
315    /// # Returns
316    /// * `Some(Stdio)` if the fd is open and can be converted to Stdio
317    /// * `None` if the fd is not open or is closed
318    #[allow(dead_code)]
319    pub fn get_stdio(&self, fd_num: i32) -> Option<Stdio> {
320        match self.fds.get(&fd_num) {
321            Some(FileDescriptor::File(file)) => {
322                // Try to duplicate the file descriptor for Stdio
323                let raw_fd = file.as_raw_fd();
324                let dup_fd = unsafe { libc::dup(raw_fd) };
325                if dup_fd >= 0 {
326                    let file = unsafe { File::from_raw_fd(dup_fd) };
327                    Some(Stdio::from(file))
328                } else {
329                    None
330                }
331            }
332            Some(FileDescriptor::Duplicate(raw_fd)) => {
333                // Duplicate the raw fd for Stdio
334                let dup_fd = unsafe { libc::dup(*raw_fd) };
335                if dup_fd >= 0 {
336                    let file = unsafe { File::from_raw_fd(dup_fd) };
337                    Some(Stdio::from(file))
338                } else {
339                    None
340                }
341            }
342            Some(FileDescriptor::Closed) | None => None,
343        }
344    }
345
346    /// Get the raw file descriptor number for a given fd
347    ///
348    /// # Arguments
349    /// * `fd_num` - The file descriptor number
350    ///
351    /// # Returns
352    /// * `Some(RawFd)` if the fd is open
353    /// * `None` if the fd is not open or is closed
354    pub fn get_raw_fd(&self, fd_num: i32) -> Option<RawFd> {
355        match self.fds.get(&fd_num) {
356            Some(FileDescriptor::File(file)) => Some(file.as_raw_fd()),
357            Some(FileDescriptor::Duplicate(raw_fd)) => Some(*raw_fd),
358            Some(FileDescriptor::Closed) => None,
359            None => {
360                // Standard file descriptors (0, 1, 2) are always open unless explicitly closed
361                if fd_num >= 0 && fd_num <= 2 {
362                    Some(fd_num as RawFd)
363                } else {
364                    None
365                }
366            }
367        }
368    }
369
370    /// Check if a file descriptor is open
371    ///
372    /// # Arguments
373    /// * `fd_num` - The file descriptor number
374    ///
375    /// # Returns
376    /// * `true` if the fd is open
377    /// * `false` if the fd is closed or not tracked
378    pub fn is_open(&self, fd_num: i32) -> bool {
379        matches!(
380            self.fds.get(&fd_num),
381            Some(FileDescriptor::File(_)) | Some(FileDescriptor::Duplicate(_))
382        )
383    }
384
385    /// Check if a file descriptor is closed
386    ///
387    /// # Arguments
388    /// * `fd_num` - The file descriptor number
389    ///
390    /// # Returns
391    /// * `true` if the fd is explicitly closed
392    /// * `false` otherwise
393    pub fn is_closed(&self, fd_num: i32) -> bool {
394        matches!(self.fds.get(&fd_num), Some(FileDescriptor::Closed))
395    }
396
397    /// Clear all file descriptors and saved state
398    pub fn clear(&mut self) {
399        self.fds.clear();
400        self.saved_fds.clear();
401    }
402}
403
404impl Default for FileDescriptorTable {
405    /// Creates the default FileDescriptorTable.
406    ///
407    /// # Examples
408    ///
409    /// ```
410    /// use rush_sh::state::FileDescriptorTable;
411    /// let table = FileDescriptorTable::default();
412    /// ```
413    fn default() -> Self {
414        Self::new()
415    }
416}
417
418/// Shell option flags that control shell behavior
419#[derive(Debug, Clone)]
420pub struct ShellOptions {
421    /// -e: Exit on command failure
422    pub errexit: bool,
423
424    /// -u: Treat unset variables as error
425    pub nounset: bool,
426
427    /// -x: Print commands before execution
428    pub xtrace: bool,
429
430    /// -v: Print input lines as read
431    pub verbose: bool,
432
433    /// -n: Read but don't execute commands
434    pub noexec: bool,
435
436    /// -f: Disable pathname expansion
437    pub noglob: bool,
438
439    /// -C: Prevent overwriting files with redirection
440    pub noclobber: bool,
441
442    /// -a: Auto-export all variables
443    pub allexport: bool,
444
445    /// -b: Notify of job completion immediately
446    pub notify: bool,
447
448    /// Ignore EOF (Ctrl+D) - not a standard POSIX option but commonly supported
449    pub ignoreeof: bool,
450
451    /// -m: Enable job control (monitor)
452    pub monitor: bool,
453}
454
455impl Default for ShellOptions {
456    /// Create a ShellOptions with all option flags set to false.
457    ///
458    /// # Examples
459    ///
460    /// ```
461    /// use rush_sh::state::ShellOptions;
462    /// let opts = ShellOptions::default();
463    /// assert!(!opts.errexit && !opts.nounset && !opts.xtrace);
464    /// ```
465    fn default() -> Self {
466        Self {
467            errexit: false,
468            nounset: false,
469            xtrace: false,
470            verbose: false,
471            noexec: false,
472            noglob: false,
473            noclobber: false,
474            allexport: false,
475            notify: false,
476            ignoreeof: false,
477            monitor: false,
478        }
479    }
480}
481
482impl ShellOptions {
483    /// Retrieve the value of a shell option by its short-name flag.
484    ///
485    /// Returns `Some(bool)` with the option's current value for recognized short names; `None` if the short name is not recognized.
486    ///
487    /// # Examples
488    ///
489    /// ```
490    /// use rush_sh::state::ShellOptions;
491    /// let opts = ShellOptions::default();
492    /// assert_eq!(opts.get_by_short_name('e'), Some(false)); // errexit is false by default
493    /// assert_eq!(opts.get_by_short_name('?'), None); // unknown short name
494    /// ```
495    #[allow(dead_code)]
496    pub fn get_by_short_name(&self, name: char) -> Option<bool> {
497        match name {
498            'e' => Some(self.errexit),
499            'u' => Some(self.nounset),
500            'x' => Some(self.xtrace),
501            'v' => Some(self.verbose),
502            'n' => Some(self.noexec),
503            'f' => Some(self.noglob),
504            'C' => Some(self.noclobber),
505            'a' => Some(self.allexport),
506            'b' => Some(self.notify),
507            'm' => Some(self.monitor),
508            _ => None,
509        }
510    }
511
512    /// Set a shell option identified by its single-character short name.
513    ///
514    /// Sets the option corresponding to `name` to `value`. Recognized short names:
515    /// 'e' (errexit), 'u' (nounset), 'x' (xtrace), 'v' (verbose), 'n' (noexec),
516    /// 'f' (noglob), 'C' (noclobber), 'a' (allexport), 'b' (notify), 'm' (monitor).
517    ///
518    /// # Arguments
519    ///
520    /// * `name` - single-character short option name.
521    /// * `value` - true to enable the option, false to disable it.
522    ///
523    /// # Returns
524    ///
525    /// `Ok(())` on success, or `Err(String)` if `name` is not a recognized option.
526    ///
527    /// # Examples
528    ///
529    /// ```
530    /// use rush_sh::state::ShellOptions;
531    /// let mut opts = ShellOptions::default();
532    /// opts.set_by_short_name('e', true).unwrap();
533    /// assert!(opts.errexit);
534    /// ```
535    pub fn set_by_short_name(&mut self, name: char, value: bool) -> Result<(), String> {
536        match name {
537            'e' => {
538                self.errexit = value;
539                Ok(())
540            }
541            'u' => {
542                self.nounset = value;
543                Ok(())
544            }
545            'x' => {
546                self.xtrace = value;
547                Ok(())
548            }
549            'v' => {
550                self.verbose = value;
551                Ok(())
552            }
553            'n' => {
554                self.noexec = value;
555                Ok(())
556            }
557            'f' => {
558                self.noglob = value;
559                Ok(())
560            }
561            'C' => {
562                self.noclobber = value;
563                Ok(())
564            }
565            'a' => {
566                self.allexport = value;
567                Ok(())
568            }
569            'b' => {
570                self.notify = value;
571                Ok(())
572            }
573            'm' => {
574                self.monitor = value;
575                Ok(())
576            }
577            _ => Err(format!("Invalid option: -{}", name)),
578        }
579    }
580
581    /// Retrieve the value of a shell option by its long name.
582    ///
583    /// `name` is the option's full identifier (for example: "errexit", "nounset", "xtrace").
584    ///
585    /// # Returns
586    ///
587    /// `Some(true)` if the option is enabled, `Some(false)` if the option is disabled, or `None` if the name is not recognized.
588    ///
589    /// # Examples
590    ///
591    /// ```
592    /// use rush_sh::state::ShellOptions;
593    /// let mut opts = ShellOptions::default();
594    /// opts.errexit = true;
595    /// assert_eq!(opts.get_by_long_name("errexit"), Some(true));
596    /// assert_eq!(opts.get_by_long_name("noglob"), Some(false));
597    /// assert_eq!(opts.get_by_long_name("unknown"), None);
598    /// ```
599    #[allow(dead_code)]
600    pub fn get_by_long_name(&self, name: &str) -> Option<bool> {
601        match name {
602            "errexit" => Some(self.errexit),
603            "nounset" => Some(self.nounset),
604            "xtrace" => Some(self.xtrace),
605            "verbose" => Some(self.verbose),
606            "noexec" => Some(self.noexec),
607            "noglob" => Some(self.noglob),
608            "noclobber" => Some(self.noclobber),
609            "allexport" => Some(self.allexport),
610            "notify" => Some(self.notify),
611            "ignoreeof" => Some(self.ignoreeof),
612            "monitor" => Some(self.monitor),
613            _ => None,
614        }
615    }
616
617    /// Set a shell option by its long name.
618    ///
619    /// Sets the specified long-form option (for example `"errexit"` or `"nounset"`) to the provided boolean value.
620    /// Returns `Ok(())` if the option was recognized and set, or `Err(String)` if the name is not recognized.
621    ///
622    /// # Examples
623    ///
624    /// ```
625    /// use rush_sh::state::ShellOptions;
626    /// let mut opts = ShellOptions::default();
627    /// opts.set_by_long_name("errexit", true).unwrap();
628    /// assert!(opts.errexit);
629    ///
630    /// assert!(opts.set_by_long_name("nonexistent", true).is_err());
631    /// ```
632    pub fn set_by_long_name(&mut self, name: &str, value: bool) -> Result<(), String> {
633        match name {
634            "errexit" => {
635                self.errexit = value;
636                Ok(())
637            }
638            "nounset" => {
639                self.nounset = value;
640                Ok(())
641            }
642            "xtrace" => {
643                self.xtrace = value;
644                Ok(())
645            }
646            "verbose" => {
647                self.verbose = value;
648                Ok(())
649            }
650            "noexec" => {
651                self.noexec = value;
652                Ok(())
653            }
654            "noglob" => {
655                self.noglob = value;
656                Ok(())
657            }
658            "noclobber" => {
659                self.noclobber = value;
660                Ok(())
661            }
662            "allexport" => {
663                self.allexport = value;
664                Ok(())
665            }
666            "notify" => {
667                self.notify = value;
668                Ok(())
669            }
670            "ignoreeof" => {
671                self.ignoreeof = value;
672                Ok(())
673            }
674            "monitor" => {
675                self.monitor = value;
676                Ok(())
677            }
678            _ => Err(format!("Invalid option: {}", name)),
679        }
680    }
681
682    /// Lists all shell option names with their short-letter aliases and current values.
683    ///
684    /// Returns a vector of tuples `(long_name, short_name, value)` for every supported option.
685    /// The `short_name` is `'\0'` when no short alias exists.
686    ///
687    /// # Examples
688    ///
689    /// ```
690    /// use rush_sh::state::ShellOptions;
691    /// let opts = ShellOptions::default();
692    /// let all = opts.get_all_options();
693    /// assert!(all.iter().any(|(name, _, _)| *name == "errexit"));
694    /// assert!(all.iter().any(|(name, short, _)| *name == "ignoreeof" && *short == '\0'));
695    /// ```
696    pub fn get_all_options(&self) -> Vec<(&'static str, char, bool)> {
697        vec![
698            ("allexport", 'a', self.allexport),
699            ("notify", 'b', self.notify),
700            ("noclobber", 'C', self.noclobber),
701            ("errexit", 'e', self.errexit),
702            ("noglob", 'f', self.noglob),
703            ("monitor", 'm', self.monitor),
704            ("noexec", 'n', self.noexec),
705            ("nounset", 'u', self.nounset),
706            ("verbose", 'v', self.verbose),
707            ("xtrace", 'x', self.xtrace),
708            ("ignoreeof", '\0', self.ignoreeof), // No short option
709        ]
710    }
711}
712
713#[derive(Debug, Clone)]
714pub struct ColorScheme {
715    /// ANSI color code for prompt
716    pub prompt: String,
717    /// ANSI color code for error messages
718    pub error: String,
719    /// ANSI color code for success messages
720    pub success: String,
721    /// ANSI color code for builtin command output
722    pub builtin: String,
723    /// ANSI color code for directory listings
724    pub directory: String,
725}
726
727impl Default for ColorScheme {
728    fn default() -> Self {
729        Self {
730            prompt: "\x1b[32m".to_string(),    // Green
731            error: "\x1b[31m".to_string(),     // Red
732            success: "\x1b[32m".to_string(),   // Green
733            builtin: "\x1b[36m".to_string(),   // Cyan
734            directory: "\x1b[34m".to_string(), // Blue
735        }
736    }
737}
738
739#[derive(Debug, Clone)]
740pub struct ShellState {
741    /// Shell variables (local to the shell session)
742    pub variables: HashMap<String, String>,
743    /// Which variables are exported to child processes
744    pub exported: HashSet<String>,
745    /// Last exit code ($?)
746    pub last_exit_code: i32,
747    /// Shell process ID ($$)
748    pub shell_pid: u32,
749    /// Script name or command ($0)
750    pub script_name: String,
751    /// Directory stack for pushd/popd
752    pub dir_stack: Vec<String>,
753    /// Command aliases
754    pub aliases: HashMap<String, String>,
755    /// Whether colors are enabled
756    pub colors_enabled: bool,
757    /// Current color scheme
758    pub color_scheme: ColorScheme,
759    /// Positional parameters ($1, $2, $3, ...)
760    pub positional_params: Vec<String>,
761    /// Function definitions
762    pub functions: HashMap<String, Ast>,
763    /// Local variable stack for function scoping
764    pub local_vars: Vec<HashMap<String, String>>,
765    /// Function call depth for local scope management
766    pub function_depth: usize,
767    /// Maximum allowed recursion depth
768    pub max_recursion_depth: usize,
769    /// Flag to indicate if we're currently returning from a function
770    pub returning: bool,
771    /// Return value when returning from a function
772    pub return_value: Option<i32>,
773    /// Loop nesting depth for break/continue
774    pub loop_depth: usize,
775    /// Flag to indicate if we're breaking out of a loop
776    pub breaking: bool,
777    /// Number of loop levels to break out of
778    pub break_level: usize,
779    /// Flag to indicate if we're continuing to next loop iteration
780    pub continuing: bool,
781    /// Number of loop levels to continue from
782    pub continue_level: usize,
783    /// Output capture buffer for command substitution
784    pub capture_output: Option<Rc<RefCell<Vec<u8>>>>,
785    /// Whether to use condensed cwd display in prompt
786    pub condensed_cwd: bool,
787    /// Signal trap handlers: maps signal name to command string
788    pub trap_handlers: Arc<Mutex<HashMap<String, String>>>,
789    /// Flag to track if EXIT trap has been executed
790    pub exit_trap_executed: bool,
791    /// Flag to indicate that the shell should exit
792    pub exit_requested: bool,
793    /// Exit code to use when exiting
794    pub exit_code: i32,
795    /// Flag to indicate pending signals need processing
796    /// Set by signal handler, checked by executor
797    #[allow(dead_code)]
798    pub pending_signals: bool,
799    /// Pending here-document content from script execution
800    pub pending_heredoc_content: Option<String>,
801    /// Interactive mode heredoc collection state
802    pub collecting_heredoc: Option<(String, String, String)>, // (command_line, delimiter, collected_content)
803    /// File descriptor table for managing open file descriptors
804    pub fd_table: Rc<RefCell<FileDescriptorTable>>,
805    /// Current subshell nesting depth (for recursion limit)
806    pub subshell_depth: usize,
807    /// Override for stdin (used for pipeline subshells to avoid process-global fd manipulation)
808    pub stdin_override: Option<RawFd>,
809    /// Shell option flags (set builtin)
810    pub options: ShellOptions,
811    /// Context tracking for errexit option - true when executing commands in if/while/until conditions
812    pub in_condition: bool,
813    /// Context tracking for errexit option - true when executing commands in && or || chains
814    pub in_logical_chain: bool,
815    /// Context tracking for errexit option - true when executing negated commands (!)
816    pub in_negation: bool,
817    /// Track if the last command executed was a negation (to skip errexit check on inverted code)
818    pub last_was_negation: bool,
819}
820
821impl ShellState {
822    /// Creates a new ShellState initialized with sensible defaults and environment-derived settings.
823    ///
824    /// The returned state initializes runtime fields (variables, exported, aliases, positional params, function/local scopes, FD table, traps, and control flags) and derives display preferences from environment:
825    /// - `colors_enabled` is determined by `NO_COLOR`, `RUSH_COLORS`, and whether stdout is a terminal.
826    /// - `condensed_cwd` is determined by `RUSH_CONDENSED` (defaults to `true`).
827    ///
828    /// # Examples
829    ///
830    /// ```
831    /// use rush_sh::ShellState;
832    /// let state = ShellState::new();
833    /// // basic invariants
834    /// assert_eq!(state.last_exit_code, 0);
835    /// assert!(state.shell_pid != 0);
836    /// ```
837    pub fn new() -> Self {
838        let shell_pid = std::process::id();
839
840        // Check NO_COLOR environment variable (respects standard)
841        let no_color = env::var("NO_COLOR").is_ok();
842
843        // Check RUSH_COLORS environment variable for explicit control
844        let rush_colors = env::var("RUSH_COLORS")
845            .map(|v| v.to_lowercase())
846            .unwrap_or_else(|_| "auto".to_string());
847
848        let colors_enabled = match rush_colors.as_str() {
849            "1" | "true" | "on" | "enable" => !no_color && std::io::stdout().is_terminal(),
850            "0" | "false" | "off" | "disable" => false,
851            "auto" => !no_color && std::io::stdout().is_terminal(),
852            _ => !no_color && std::io::stdout().is_terminal(),
853        };
854
855        // Check RUSH_CONDENSED environment variable for cwd display preference
856        let rush_condensed = env::var("RUSH_CONDENSED")
857            .map(|v| v.to_lowercase())
858            .unwrap_or_else(|_| "true".to_string());
859
860        let condensed_cwd = match rush_condensed.as_str() {
861            "1" | "true" | "on" | "enable" => true,
862            "0" | "false" | "off" | "disable" => false,
863            _ => true, // Default to condensed for backward compatibility
864        };
865
866        Self {
867            variables: HashMap::new(),
868            exported: HashSet::new(),
869            last_exit_code: 0,
870            shell_pid,
871            script_name: "rush".to_string(),
872            dir_stack: Vec::new(),
873            aliases: HashMap::new(),
874            colors_enabled,
875            color_scheme: ColorScheme::default(),
876            positional_params: Vec::new(),
877            functions: HashMap::new(),
878            local_vars: Vec::new(),
879            function_depth: 0,
880            max_recursion_depth: 500, // Default recursion limit (reduced to avoid Rust stack overflow)
881            returning: false,
882            return_value: None,
883            loop_depth: 0,
884            breaking: false,
885            break_level: 0,
886            continuing: false,
887            continue_level: 0,
888            capture_output: None,
889            condensed_cwd,
890            trap_handlers: Arc::new(Mutex::new(HashMap::new())),
891            exit_trap_executed: false,
892            exit_requested: false,
893            exit_code: 0,
894            pending_signals: false,
895            pending_heredoc_content: None,
896            collecting_heredoc: None,
897            fd_table: Rc::new(RefCell::new(FileDescriptorTable::new())),
898            subshell_depth: 0,
899            stdin_override: None,
900            options: ShellOptions::default(),
901            in_condition: false,
902            in_logical_chain: false,
903            in_negation: false,
904            last_was_negation: false,
905        }
906    }
907
908    /// Get a variable value, checking local scopes first, then shell variables, then environment
909    pub fn get_var(&self, name: &str) -> Option<String> {
910        // Handle special variables (these are never local)
911        match name {
912            "?" => Some(self.last_exit_code.to_string()),
913            "$" => Some(self.shell_pid.to_string()),
914            "0" => Some(self.script_name.clone()),
915            "*" => {
916                // $* - all positional parameters as single string (space-separated)
917                if self.positional_params.is_empty() {
918                    Some("".to_string())
919                } else {
920                    Some(self.positional_params.join(" "))
921                }
922            }
923            "@" => {
924                // $@ - all positional parameters as separate words (but returns as single string for compatibility)
925                if self.positional_params.is_empty() {
926                    Some("".to_string())
927                } else {
928                    Some(self.positional_params.join(" "))
929                }
930            }
931            "#" => Some(self.positional_params.len().to_string()),
932            _ => {
933                // Handle positional parameters $1, $2, $3, etc. (these are never local)
934                if let Ok(index) = name.parse::<usize>()
935                    && index > 0
936                    && index <= self.positional_params.len()
937                {
938                    return Some(self.positional_params[index - 1].clone());
939                }
940
941                // Check local scopes first, then shell variables, then environment
942                // Search local scopes from innermost to outermost
943                for scope in self.local_vars.iter().rev() {
944                    if let Some(value) = scope.get(name) {
945                        return Some(value.clone());
946                    }
947                }
948
949                // Check shell variables
950                if let Some(value) = self.variables.get(name) {
951                    Some(value.clone())
952                } else {
953                    // Fall back to environment variables
954                    env::var(name).ok()
955                }
956            }
957        }
958    }
959
960    /// Set a shell variable (updates local scope if variable exists there, otherwise sets globally)
961    pub fn set_var(&mut self, name: &str, value: String) {
962        // Check if this variable exists in any local scope
963        // If it does, update it there instead of setting globally
964        for scope in self.local_vars.iter_mut().rev() {
965            if scope.contains_key(name) {
966                scope.insert(name.to_string(), value);
967                return;
968            }
969        }
970
971        // Variable doesn't exist in local scopes, set it globally
972        self.variables.insert(name.to_string(), value);
973    }
974
975    /// Remove a shell variable
976    pub fn unset_var(&mut self, name: &str) {
977        self.variables.remove(name);
978        self.exported.remove(name);
979    }
980
981    /// Mark a variable as exported
982    pub fn export_var(&mut self, name: &str) {
983        if self.variables.contains_key(name) {
984            self.exported.insert(name.to_string());
985        }
986    }
987
988    /// Set and export a variable
989    pub fn set_exported_var(&mut self, name: &str, value: String) {
990        self.set_var(name, value);
991        self.export_var(name);
992    }
993
994    /// Get all environment variables for child processes (exported + inherited)
995    pub fn get_env_for_child(&self) -> HashMap<String, String> {
996        let mut child_env = HashMap::new();
997
998        // Add all current environment variables
999        for (key, value) in env::vars() {
1000            child_env.insert(key, value);
1001        }
1002
1003        // Override with exported shell variables
1004        for var_name in &self.exported {
1005            if let Some(value) = self.variables.get(var_name) {
1006                child_env.insert(var_name.clone(), value.clone());
1007            }
1008        }
1009
1010        child_env
1011    }
1012
1013    /// Update the last exit code
1014    pub fn set_last_exit_code(&mut self, code: i32) {
1015        self.last_exit_code = code;
1016    }
1017
1018    /// Set the script name ($0)
1019    pub fn set_script_name(&mut self, name: &str) {
1020        self.script_name = name.to_string();
1021    }
1022
1023    /// Get the condensed current working directory for the prompt
1024    pub fn get_condensed_cwd(&self) -> String {
1025        match env::current_dir() {
1026            Ok(path) => {
1027                let path_str = path.to_string_lossy();
1028                let components: Vec<&str> = path_str.split('/').collect();
1029                if components.is_empty() || (components.len() == 1 && components[0].is_empty()) {
1030                    return "/".to_string();
1031                }
1032                let mut result = String::new();
1033                for (i, comp) in components.iter().enumerate() {
1034                    if comp.is_empty() {
1035                        continue; // skip leading empty component
1036                    }
1037                    if i == components.len() - 1 {
1038                        result.push('/');
1039                        result.push_str(comp);
1040                    } else {
1041                        result.push('/');
1042                        if let Some(first) = comp.chars().next() {
1043                            result.push(first);
1044                        }
1045                    }
1046                }
1047                if result.is_empty() {
1048                    "/".to_string()
1049                } else {
1050                    result
1051                }
1052            }
1053            Err(_) => "/?".to_string(), // fallback if can't get cwd
1054        }
1055    }
1056
1057    /// Get the full current working directory for the prompt
1058    pub fn get_full_cwd(&self) -> String {
1059        match env::current_dir() {
1060            Ok(path) => path.to_string_lossy().to_string(),
1061            Err(_) => "/?".to_string(), // fallback if can't get cwd
1062        }
1063    }
1064
1065    /// Get the user@hostname string for the prompt
1066    pub fn get_user_hostname(&self) -> String {
1067        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
1068
1069        // First try to get hostname from HOSTNAME environment variable
1070        if let Ok(hostname) = env::var("HOSTNAME")
1071            && !hostname.trim().is_empty()
1072        {
1073            return format!("{}@{}", user, hostname);
1074        }
1075
1076        // If HOSTNAME is not set or empty, try the hostname command
1077        let hostname = match std::process::Command::new("hostname").output() {
1078            Ok(output) if output.status.success() => {
1079                String::from_utf8_lossy(&output.stdout).trim().to_string()
1080            }
1081            _ => "hostname".to_string(), // Last resort fallback
1082        };
1083
1084        // Set the HOSTNAME environment variable for future use
1085        if hostname != "hostname" {
1086            unsafe {
1087                env::set_var("HOSTNAME", &hostname);
1088            }
1089        }
1090
1091        format!("{}@{}", user, hostname)
1092    }
1093
1094    /// Get the full prompt string
1095    pub fn get_prompt(&self) -> String {
1096        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
1097        let prompt_char = if user == "root" { "#" } else { "$" };
1098        let cwd = if self.condensed_cwd {
1099            self.get_condensed_cwd()
1100        } else {
1101            self.get_full_cwd()
1102        };
1103        format!("{}:{} {} ", self.get_user_hostname(), cwd, prompt_char)
1104    }
1105
1106    /// Set an alias
1107    pub fn set_alias(&mut self, name: &str, value: String) {
1108        self.aliases.insert(name.to_string(), value);
1109    }
1110
1111    /// Get an alias value
1112    pub fn get_alias(&self, name: &str) -> Option<&String> {
1113        self.aliases.get(name)
1114    }
1115
1116    /// Remove an alias
1117    pub fn remove_alias(&mut self, name: &str) {
1118        self.aliases.remove(name);
1119    }
1120
1121    /// Get all aliases
1122    pub fn get_all_aliases(&self) -> &HashMap<String, String> {
1123        &self.aliases
1124    }
1125
1126    /// Set positional parameters
1127    pub fn set_positional_params(&mut self, params: Vec<String>) {
1128        self.positional_params = params;
1129    }
1130
1131    /// Get positional parameters
1132    #[allow(dead_code)]
1133    pub fn get_positional_params(&self) -> &[String] {
1134        &self.positional_params
1135    }
1136
1137    /// Shift positional parameters (remove first n parameters)
1138    pub fn shift_positional_params(&mut self, count: usize) {
1139        if count > 0 {
1140            for _ in 0..count {
1141                if !self.positional_params.is_empty() {
1142                    self.positional_params.remove(0);
1143                }
1144            }
1145        }
1146    }
1147
1148    /// Add a positional parameter at the end
1149    #[allow(dead_code)]
1150    pub fn push_positional_param(&mut self, param: String) {
1151        self.positional_params.push(param);
1152    }
1153
1154    /// Define a function
1155    pub fn define_function(&mut self, name: String, body: Ast) {
1156        self.functions.insert(name, body);
1157    }
1158
1159    /// Get a function definition
1160    pub fn get_function(&self, name: &str) -> Option<&Ast> {
1161        self.functions.get(name)
1162    }
1163
1164    /// Remove a function definition
1165    #[allow(dead_code)]
1166    pub fn remove_function(&mut self, name: &str) {
1167        self.functions.remove(name);
1168    }
1169
1170    /// Get all function names
1171    #[allow(dead_code)]
1172    pub fn get_function_names(&self) -> Vec<&String> {
1173        self.functions.keys().collect()
1174    }
1175
1176    /// Push a new local variable scope
1177    pub fn push_local_scope(&mut self) {
1178        self.local_vars.push(HashMap::new());
1179    }
1180
1181    /// Pop the current local variable scope
1182    pub fn pop_local_scope(&mut self) {
1183        if !self.local_vars.is_empty() {
1184            self.local_vars.pop();
1185        }
1186    }
1187
1188    /// Set a local variable in the current scope
1189    pub fn set_local_var(&mut self, name: &str, value: String) {
1190        if let Some(current_scope) = self.local_vars.last_mut() {
1191            current_scope.insert(name.to_string(), value);
1192        } else {
1193            // If no local scope exists, set as global variable
1194            self.set_var(name, value);
1195        }
1196    }
1197
1198    /// Enter a function context (push local scope if needed)
1199    pub fn enter_function(&mut self) {
1200        self.function_depth += 1;
1201        if self.function_depth > self.local_vars.len() {
1202            self.push_local_scope();
1203        }
1204    }
1205
1206    /// Exit a function context (pop local scope if needed)
1207    pub fn exit_function(&mut self) {
1208        if self.function_depth > 0 {
1209            self.function_depth -= 1;
1210            if self.function_depth == self.local_vars.len() - 1 {
1211                self.pop_local_scope();
1212            }
1213        }
1214    }
1215
1216    /// Set return state for function returns
1217    pub fn set_return(&mut self, value: i32) {
1218        self.returning = true;
1219        self.return_value = Some(value);
1220    }
1221
1222    /// Clear return state
1223    pub fn clear_return(&mut self) {
1224        self.returning = false;
1225        self.return_value = None;
1226    }
1227
1228    /// Check if currently returning
1229    pub fn is_returning(&self) -> bool {
1230        self.returning
1231    }
1232
1233    /// Get return value if returning
1234    pub fn get_return_value(&self) -> Option<i32> {
1235        self.return_value
1236    }
1237
1238    /// Enter a loop context (increment loop depth)
1239    pub fn enter_loop(&mut self) {
1240        self.loop_depth += 1;
1241    }
1242
1243    /// Exit a loop context (decrement loop depth)
1244    pub fn exit_loop(&mut self) {
1245        if self.loop_depth > 0 {
1246            self.loop_depth -= 1;
1247        }
1248    }
1249
1250    /// Set break state for loop control
1251    pub fn set_break(&mut self, level: usize) {
1252        self.breaking = true;
1253        self.break_level = level;
1254    }
1255
1256    /// Clear break state
1257    pub fn clear_break(&mut self) {
1258        self.breaking = false;
1259        self.break_level = 0;
1260    }
1261
1262    /// Check if currently breaking
1263    pub fn is_breaking(&self) -> bool {
1264        self.breaking
1265    }
1266
1267    /// Get break level
1268    pub fn get_break_level(&self) -> usize {
1269        self.break_level
1270    }
1271
1272    /// Decrement break level (when exiting a loop level)
1273    pub fn decrement_break_level(&mut self) {
1274        if self.break_level > 0 {
1275            self.break_level -= 1;
1276        }
1277        if self.break_level == 0 {
1278            self.breaking = false;
1279        }
1280    }
1281
1282    /// Set continue state for loop control
1283    pub fn set_continue(&mut self, level: usize) {
1284        self.continuing = true;
1285        self.continue_level = level;
1286    }
1287
1288    /// Clear continue state
1289    pub fn clear_continue(&mut self) {
1290        self.continuing = false;
1291        self.continue_level = 0;
1292    }
1293
1294    /// Check if currently continuing
1295    pub fn is_continuing(&self) -> bool {
1296        self.continuing
1297    }
1298
1299    /// Get continue level
1300    pub fn get_continue_level(&self) -> usize {
1301        self.continue_level
1302    }
1303
1304    /// Decrement continue level (when exiting a loop level)
1305    pub fn decrement_continue_level(&mut self) {
1306        if self.continue_level > 0 {
1307            self.continue_level -= 1;
1308        }
1309        if self.continue_level == 0 {
1310            self.continuing = false;
1311        }
1312    }
1313
1314    /// Set a trap handler for a signal
1315    pub fn set_trap(&mut self, signal: &str, command: String) {
1316        if let Ok(mut handlers) = self.trap_handlers.lock() {
1317            handlers.insert(signal.to_uppercase(), command);
1318        }
1319    }
1320
1321    /// Get a trap handler for a signal
1322    pub fn get_trap(&self, signal: &str) -> Option<String> {
1323        if let Ok(handlers) = self.trap_handlers.lock() {
1324            handlers.get(&signal.to_uppercase()).cloned()
1325        } else {
1326            None
1327        }
1328    }
1329
1330    /// Remove a trap handler for a signal
1331    pub fn remove_trap(&mut self, signal: &str) {
1332        if let Ok(mut handlers) = self.trap_handlers.lock() {
1333            handlers.remove(&signal.to_uppercase());
1334        }
1335    }
1336
1337    /// Get all trap handlers
1338    pub fn get_all_traps(&self) -> HashMap<String, String> {
1339        if let Ok(handlers) = self.trap_handlers.lock() {
1340            handlers.clone()
1341        } else {
1342            HashMap::new()
1343        }
1344    }
1345
1346    /// Clear all trap handlers
1347    #[allow(dead_code)]
1348    pub fn clear_traps(&mut self) {
1349        if let Ok(mut handlers) = self.trap_handlers.lock() {
1350            handlers.clear();
1351        }
1352    }
1353}
1354
1355/// Enqueue a signal event for later processing
1356/// If the queue is full, the oldest event is dropped
1357pub fn enqueue_signal(signal_name: &str, signal_number: i32) {
1358    if let Ok(mut queue) = SIGNAL_QUEUE.lock() {
1359        // If queue is full, remove oldest event
1360        if queue.len() >= MAX_SIGNAL_QUEUE_SIZE {
1361            queue.pop_front();
1362            eprintln!("Warning: Signal queue overflow, dropping oldest signal");
1363        }
1364
1365        queue.push_back(SignalEvent::new(signal_name.to_string(), signal_number));
1366    }
1367}
1368
1369/// Process all pending signals in the queue
1370/// This should be called at safe points during command execution
1371pub fn process_pending_signals(shell_state: &mut ShellState) {
1372    // Try to lock the queue with a timeout to avoid blocking
1373    if let Ok(mut queue) = SIGNAL_QUEUE.lock() {
1374        // Process all pending signals
1375        while let Some(signal_event) = queue.pop_front() {
1376            // Check if a trap is set for this signal
1377            if let Some(trap_cmd) = shell_state.get_trap(&signal_event.signal_name)
1378                && !trap_cmd.is_empty()
1379            {
1380                // Display signal information for debugging/tracking
1381                if shell_state.colors_enabled {
1382                    eprintln!(
1383                        "{}Signal {} (signal {}) received at {:?}\x1b[0m",
1384                        shell_state.color_scheme.builtin,
1385                        signal_event.signal_name,
1386                        signal_event.signal_number,
1387                        signal_event.timestamp
1388                    );
1389                } else {
1390                    eprintln!(
1391                        "Signal {} (signal {}) received at {:?}",
1392                        signal_event.signal_name,
1393                        signal_event.signal_number,
1394                        signal_event.timestamp
1395                    );
1396                }
1397
1398                // Execute the trap handler
1399                // Note: This preserves the exit code as per POSIX requirements
1400                crate::executor::execute_trap_handler(&trap_cmd, shell_state);
1401            }
1402        }
1403    }
1404}
1405
1406impl Default for ShellState {
1407    fn default() -> Self {
1408        Self::new()
1409    }
1410}
1411
1412#[cfg(test)]
1413mod tests {
1414    use super::*;
1415    use std::sync::Mutex;
1416
1417    // Mutex to serialize tests that create temporary files
1418    static FILE_LOCK: Mutex<()> = Mutex::new(());
1419
1420    #[test]
1421    fn test_shell_state_basic() {
1422        let mut state = ShellState::new();
1423        state.set_var("TEST_VAR", "test_value".to_string());
1424        assert_eq!(state.get_var("TEST_VAR"), Some("test_value".to_string()));
1425    }
1426
1427    #[test]
1428    fn test_special_variables() {
1429        let mut state = ShellState::new();
1430        state.set_last_exit_code(42);
1431        state.set_script_name("test_script");
1432
1433        assert_eq!(state.get_var("?"), Some("42".to_string()));
1434        assert_eq!(state.get_var("$"), Some(state.shell_pid.to_string()));
1435        assert_eq!(state.get_var("0"), Some("test_script".to_string()));
1436    }
1437
1438    #[test]
1439    fn test_export_variable() {
1440        let mut state = ShellState::new();
1441        state.set_var("EXPORT_VAR", "export_value".to_string());
1442        state.export_var("EXPORT_VAR");
1443
1444        let child_env = state.get_env_for_child();
1445        assert_eq!(
1446            child_env.get("EXPORT_VAR"),
1447            Some(&"export_value".to_string())
1448        );
1449    }
1450
1451    #[test]
1452    fn test_unset_variable() {
1453        let mut state = ShellState::new();
1454        state.set_var("UNSET_VAR", "value".to_string());
1455        state.export_var("UNSET_VAR");
1456
1457        assert!(state.variables.contains_key("UNSET_VAR"));
1458        assert!(state.exported.contains("UNSET_VAR"));
1459
1460        state.unset_var("UNSET_VAR");
1461
1462        assert!(!state.variables.contains_key("UNSET_VAR"));
1463        assert!(!state.exported.contains("UNSET_VAR"));
1464    }
1465
1466    #[test]
1467    fn test_get_user_hostname() {
1468        let state = ShellState::new();
1469        let user_hostname = state.get_user_hostname();
1470        // Should contain @ since it's user@hostname format
1471        assert!(user_hostname.contains('@'));
1472    }
1473
1474    #[test]
1475    fn test_get_prompt() {
1476        let state = ShellState::new();
1477        let prompt = state.get_prompt();
1478        // Should end with $ and contain @
1479        assert!(prompt.ends_with(" $ "));
1480        assert!(prompt.contains('@'));
1481    }
1482
1483    #[test]
1484    fn test_positional_parameters() {
1485        let mut state = ShellState::new();
1486        state.set_positional_params(vec![
1487            "arg1".to_string(),
1488            "arg2".to_string(),
1489            "arg3".to_string(),
1490        ]);
1491
1492        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
1493        assert_eq!(state.get_var("2"), Some("arg2".to_string()));
1494        assert_eq!(state.get_var("3"), Some("arg3".to_string()));
1495        assert_eq!(state.get_var("4"), None);
1496        assert_eq!(state.get_var("#"), Some("3".to_string()));
1497        assert_eq!(state.get_var("*"), Some("arg1 arg2 arg3".to_string()));
1498        assert_eq!(state.get_var("@"), Some("arg1 arg2 arg3".to_string()));
1499    }
1500
1501    #[test]
1502    fn test_positional_parameters_empty() {
1503        let mut state = ShellState::new();
1504        state.set_positional_params(vec![]);
1505
1506        assert_eq!(state.get_var("1"), None);
1507        assert_eq!(state.get_var("#"), Some("0".to_string()));
1508        assert_eq!(state.get_var("*"), Some("".to_string()));
1509        assert_eq!(state.get_var("@"), Some("".to_string()));
1510    }
1511
1512    #[test]
1513    fn test_shift_positional_params() {
1514        let mut state = ShellState::new();
1515        state.set_positional_params(vec![
1516            "arg1".to_string(),
1517            "arg2".to_string(),
1518            "arg3".to_string(),
1519        ]);
1520
1521        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
1522        assert_eq!(state.get_var("2"), Some("arg2".to_string()));
1523        assert_eq!(state.get_var("3"), Some("arg3".to_string()));
1524
1525        state.shift_positional_params(1);
1526
1527        assert_eq!(state.get_var("1"), Some("arg2".to_string()));
1528        assert_eq!(state.get_var("2"), Some("arg3".to_string()));
1529        assert_eq!(state.get_var("3"), None);
1530        assert_eq!(state.get_var("#"), Some("2".to_string()));
1531
1532        state.shift_positional_params(2);
1533
1534        assert_eq!(state.get_var("1"), None);
1535        assert_eq!(state.get_var("#"), Some("0".to_string()));
1536    }
1537
1538    #[test]
1539    fn test_push_positional_param() {
1540        let mut state = ShellState::new();
1541        state.set_positional_params(vec!["arg1".to_string()]);
1542
1543        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
1544        assert_eq!(state.get_var("#"), Some("1".to_string()));
1545
1546        state.push_positional_param("arg2".to_string());
1547
1548        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
1549        assert_eq!(state.get_var("2"), Some("arg2".to_string()));
1550        assert_eq!(state.get_var("#"), Some("2".to_string()));
1551    }
1552
1553    #[test]
1554    fn test_local_variable_scoping() {
1555        let mut state = ShellState::new();
1556
1557        // Set a global variable
1558        state.set_var("global_var", "global_value".to_string());
1559        assert_eq!(
1560            state.get_var("global_var"),
1561            Some("global_value".to_string())
1562        );
1563
1564        // Push local scope
1565        state.push_local_scope();
1566
1567        // Set a local variable with the same name
1568        state.set_local_var("global_var", "local_value".to_string());
1569        assert_eq!(state.get_var("global_var"), Some("local_value".to_string()));
1570
1571        // Set another local variable
1572        state.set_local_var("local_var", "local_only".to_string());
1573        assert_eq!(state.get_var("local_var"), Some("local_only".to_string()));
1574
1575        // Pop local scope
1576        state.pop_local_scope();
1577
1578        // Should be back to global variable
1579        assert_eq!(
1580            state.get_var("global_var"),
1581            Some("global_value".to_string())
1582        );
1583        assert_eq!(state.get_var("local_var"), None);
1584    }
1585
1586    #[test]
1587    fn test_nested_local_scopes() {
1588        let mut state = ShellState::new();
1589
1590        // Set global variable
1591        state.set_var("test_var", "global".to_string());
1592
1593        // Push first local scope
1594        state.push_local_scope();
1595        state.set_local_var("test_var", "level1".to_string());
1596        assert_eq!(state.get_var("test_var"), Some("level1".to_string()));
1597
1598        // Push second local scope
1599        state.push_local_scope();
1600        state.set_local_var("test_var", "level2".to_string());
1601        assert_eq!(state.get_var("test_var"), Some("level2".to_string()));
1602
1603        // Pop second scope
1604        state.pop_local_scope();
1605        assert_eq!(state.get_var("test_var"), Some("level1".to_string()));
1606
1607        // Pop first scope
1608        state.pop_local_scope();
1609        assert_eq!(state.get_var("test_var"), Some("global".to_string()));
1610    }
1611
1612    #[test]
1613    fn test_variable_set_in_local_scope() {
1614        let mut state = ShellState::new();
1615
1616        // No local scope initially
1617        state.set_var("test_var", "global".to_string());
1618        assert_eq!(state.get_var("test_var"), Some("global".to_string()));
1619
1620        // Push local scope and set local variable
1621        state.push_local_scope();
1622        state.set_local_var("test_var", "local".to_string());
1623        assert_eq!(state.get_var("test_var"), Some("local".to_string()));
1624
1625        // Pop scope
1626        state.pop_local_scope();
1627        assert_eq!(state.get_var("test_var"), Some("global".to_string()));
1628    }
1629
1630    #[test]
1631    fn test_condensed_cwd_environment_variable() {
1632        // Test default behavior (should be true for backward compatibility)
1633        let state = ShellState::new();
1634        assert!(state.condensed_cwd);
1635
1636        // Test explicit true
1637        unsafe {
1638            env::set_var("RUSH_CONDENSED", "true");
1639        }
1640        let state = ShellState::new();
1641        assert!(state.condensed_cwd);
1642
1643        // Test explicit false
1644        unsafe {
1645            env::set_var("RUSH_CONDENSED", "false");
1646        }
1647        let state = ShellState::new();
1648        assert!(!state.condensed_cwd);
1649
1650        // Clean up
1651        unsafe {
1652            env::remove_var("RUSH_CONDENSED");
1653        }
1654    }
1655
1656    #[test]
1657    fn test_get_full_cwd() {
1658        let state = ShellState::new();
1659        let full_cwd = state.get_full_cwd();
1660        assert!(!full_cwd.is_empty());
1661        // Should contain path separators (either / or \ depending on platform)
1662        assert!(full_cwd.contains('/') || full_cwd.contains('\\'));
1663    }
1664
1665    #[test]
1666    fn test_prompt_with_condensed_setting() {
1667        let mut state = ShellState::new();
1668
1669        // Test with condensed enabled (default)
1670        assert!(state.condensed_cwd);
1671        let prompt_condensed = state.get_prompt();
1672        assert!(prompt_condensed.contains('@'));
1673
1674        // Test with condensed disabled
1675        state.condensed_cwd = false;
1676        let prompt_full = state.get_prompt();
1677        assert!(prompt_full.contains('@'));
1678
1679        // Both should end with "$ " (or "# " for root)
1680        assert!(prompt_condensed.ends_with("$ ") || prompt_condensed.ends_with("# "));
1681        assert!(prompt_full.ends_with("$ ") || prompt_full.ends_with("# "));
1682    }
1683
1684    // File Descriptor Table Tests
1685
1686    #[test]
1687    fn test_fd_table_creation() {
1688        let fd_table = FileDescriptorTable::new();
1689        assert!(!fd_table.is_open(0));
1690        assert!(!fd_table.is_open(1));
1691        assert!(!fd_table.is_open(2));
1692    }
1693
1694    #[test]
1695    fn test_fd_table_open_file() {
1696        let mut fd_table = FileDescriptorTable::new();
1697
1698        // Create a temporary file
1699        let temp_file = "/tmp/rush_test_fd_open.txt";
1700        std::fs::write(temp_file, "test content").unwrap();
1701
1702        // Open file for reading
1703        let result = fd_table.open_fd(3, temp_file, true, false, false, false, false);
1704        assert!(result.is_ok());
1705        assert!(fd_table.is_open(3));
1706
1707        // Clean up
1708        let _ = std::fs::remove_file(temp_file);
1709    }
1710
1711    #[test]
1712    fn test_fd_table_open_file_for_writing() {
1713        let mut fd_table = FileDescriptorTable::new();
1714
1715        // Create a temporary file path
1716        let temp_file = "/tmp/rush_test_fd_write.txt";
1717
1718        // Open file for writing
1719        let result = fd_table.open_fd(4, temp_file, false, true, false, true, false);
1720        assert!(result.is_ok());
1721        assert!(fd_table.is_open(4));
1722
1723        // Clean up
1724        let _ = std::fs::remove_file(temp_file);
1725    }
1726
1727    #[test]
1728    fn test_fd_table_invalid_fd_number() {
1729        let mut fd_table = FileDescriptorTable::new();
1730
1731        // Test invalid fd numbers
1732        let result = fd_table.open_fd(-1, "/tmp/test.txt", true, false, false, false, false);
1733        assert!(result.is_err());
1734        assert!(result.unwrap_err().contains("Invalid file descriptor"));
1735
1736        let result = fd_table.open_fd(1025, "/tmp/test.txt", true, false, false, false, false);
1737        assert!(result.is_err());
1738        assert!(result.unwrap_err().contains("Invalid file descriptor"));
1739    }
1740
1741    #[test]
1742    fn test_fd_table_duplicate_fd() {
1743        let mut fd_table = FileDescriptorTable::new();
1744
1745        // Create a temporary file
1746        let temp_file = "/tmp/rush_test_fd_dup.txt";
1747        std::fs::write(temp_file, "test content").unwrap();
1748
1749        // Open file on fd 3
1750        fd_table
1751            .open_fd(3, temp_file, true, false, false, false, false)
1752            .unwrap();
1753        assert!(fd_table.is_open(3));
1754
1755        // Duplicate fd 3 to fd 4
1756        let result = fd_table.duplicate_fd(3, 4);
1757        assert!(result.is_ok());
1758        assert!(fd_table.is_open(4));
1759
1760        // Clean up
1761        let _ = std::fs::remove_file(temp_file);
1762    }
1763
1764    #[test]
1765    fn test_fd_table_duplicate_to_self() {
1766        let mut fd_table = FileDescriptorTable::new();
1767
1768        // Create a temporary file
1769        let temp_file = "/tmp/rush_test_fd_dup_self.txt";
1770        std::fs::write(temp_file, "test content").unwrap();
1771
1772        // Open file on fd 3
1773        fd_table
1774            .open_fd(3, temp_file, true, false, false, false, false)
1775            .unwrap();
1776
1777        // Duplicate fd 3 to itself (should be no-op)
1778        let result = fd_table.duplicate_fd(3, 3);
1779        assert!(result.is_ok());
1780        assert!(fd_table.is_open(3));
1781
1782        // Clean up
1783        let _ = std::fs::remove_file(temp_file);
1784    }
1785
1786    #[test]
1787    fn test_fd_table_duplicate_closed_fd() {
1788        let mut fd_table = FileDescriptorTable::new();
1789
1790        // Try to duplicate a closed fd
1791        let result = fd_table.duplicate_fd(3, 4);
1792        assert!(result.is_err());
1793        assert!(result.unwrap_err().contains("not open"));
1794    }
1795
1796    #[test]
1797    fn test_fd_table_close_fd() {
1798        let mut fd_table = FileDescriptorTable::new();
1799
1800        // Create a temporary file
1801        let temp_file = "/tmp/rush_test_fd_close.txt";
1802        std::fs::write(temp_file, "test content").unwrap();
1803
1804        // Open file on fd 3
1805        fd_table
1806            .open_fd(3, temp_file, true, false, false, false, false)
1807            .unwrap();
1808        assert!(fd_table.is_open(3));
1809
1810        // Close fd 3
1811        let result = fd_table.close_fd(3);
1812        assert!(result.is_ok());
1813        assert!(fd_table.is_closed(3));
1814        assert!(!fd_table.is_open(3));
1815
1816        // Clean up
1817        let _ = std::fs::remove_file(temp_file);
1818    }
1819
1820    #[test]
1821    fn test_fd_table_save_and_restore() {
1822        let mut fd_table = FileDescriptorTable::new();
1823
1824        // Save stdin (fd 0)
1825        let result = fd_table.save_fd(0);
1826        assert!(result.is_ok());
1827
1828        // Restore stdin
1829        let result = fd_table.restore_fd(0);
1830        assert!(result.is_ok());
1831    }
1832
1833    #[test]
1834    fn test_fd_table_save_all_and_restore_all() {
1835        let _lock = FILE_LOCK.lock().unwrap();
1836        let mut fd_table = FileDescriptorTable::new();
1837
1838        // Create unique temporary files
1839        use std::time::{SystemTime, UNIX_EPOCH};
1840        let timestamp = SystemTime::now()
1841            .duration_since(UNIX_EPOCH)
1842            .unwrap()
1843            .as_nanos();
1844        let temp_file1 = format!("/tmp/rush_test_fd_save1_{}.txt", timestamp);
1845        let temp_file2 = format!("/tmp/rush_test_fd_save2_{}.txt", timestamp);
1846
1847        std::fs::write(&temp_file1, "test content 1").unwrap();
1848        std::fs::write(&temp_file2, "test content 2").unwrap();
1849
1850        // Open files on fd 50 and 51
1851        // Manually dup2 to ensure these FDs are valid for save_fd()
1852        // Using higher FDs to avoid conflict with parallel tests using 0-9
1853        let f1 = File::open(&temp_file1).unwrap();
1854        let f2 = File::open(&temp_file2).unwrap();
1855        unsafe {
1856            libc::dup2(f1.as_raw_fd(), 50);
1857            libc::dup2(f2.as_raw_fd(), 51);
1858        }
1859
1860        fd_table
1861            .open_fd(50, &temp_file1, true, false, false, false, false)
1862            .unwrap();
1863        fd_table
1864            .open_fd(51, &temp_file2, true, false, false, false, false)
1865            .unwrap();
1866
1867        // Save all fds
1868        let result = fd_table.save_all_fds();
1869        assert!(result.is_ok());
1870
1871        // Restore all fds
1872        let result = fd_table.restore_all_fds();
1873        assert!(result.is_ok());
1874
1875        // Clean up
1876        unsafe {
1877            libc::close(50);
1878            libc::close(51);
1879        }
1880        let _ = std::fs::remove_file(&temp_file1);
1881        let _ = std::fs::remove_file(&temp_file2);
1882    }
1883
1884    #[test]
1885    fn test_fd_table_clear() {
1886        let mut fd_table = FileDescriptorTable::new();
1887
1888        // Create a temporary file
1889        let temp_file = "/tmp/rush_test_fd_clear.txt";
1890        std::fs::write(temp_file, "test content").unwrap();
1891
1892        // Open file on fd 50 (was 3)
1893        // Manual setup not strictly needed for clear() test as it checks map?
1894        // But clear() might close FDs?
1895        // FileDescriptorTable::clear() just clears map. File drops.
1896
1897        fd_table
1898            .open_fd(50, temp_file, true, false, false, false, false)
1899            .unwrap();
1900        assert!(fd_table.is_open(50));
1901
1902        // Clear all fds
1903        fd_table.clear();
1904        assert!(!fd_table.is_open(3));
1905
1906        // Clean up
1907        let _ = std::fs::remove_file(temp_file);
1908    }
1909
1910    #[test]
1911    fn test_fd_table_get_stdio() {
1912        let mut fd_table = FileDescriptorTable::new();
1913
1914        // Create a temporary file
1915        let temp_file = "/tmp/rush_test_fd_stdio.txt";
1916        std::fs::write(temp_file, "test content").unwrap();
1917
1918        // Open file on fd 3
1919        fd_table
1920            .open_fd(3, temp_file, true, false, false, false, false)
1921            .unwrap();
1922
1923        // Get Stdio for fd 3
1924        let stdio = fd_table.get_stdio(3);
1925        assert!(stdio.is_some());
1926
1927        // Get Stdio for non-existent fd
1928        let stdio = fd_table.get_stdio(5);
1929        assert!(stdio.is_none());
1930
1931        // Clean up
1932        let _ = std::fs::remove_file(temp_file);
1933    }
1934
1935    #[test]
1936    fn test_fd_table_multiple_operations() {
1937        let mut fd_table = FileDescriptorTable::new();
1938
1939        // Create temporary files
1940        let temp_file1 = "/tmp/rush_test_fd_multi1.txt";
1941        let temp_file2 = "/tmp/rush_test_fd_multi2.txt";
1942        std::fs::write(temp_file1, "test content 1").unwrap();
1943        std::fs::write(temp_file2, "test content 2").unwrap();
1944
1945        // Open file on fd 3
1946        fd_table
1947            .open_fd(3, temp_file1, true, false, false, false, false)
1948            .unwrap();
1949        assert!(fd_table.is_open(3));
1950
1951        // Duplicate fd 3 to fd 4
1952        fd_table.duplicate_fd(3, 4).unwrap();
1953        assert!(fd_table.is_open(4));
1954
1955        // Open another file on fd 5
1956        fd_table
1957            .open_fd(5, temp_file2, true, false, false, false, false)
1958            .unwrap();
1959        assert!(fd_table.is_open(5));
1960
1961        // Close fd 4
1962        fd_table.close_fd(4).unwrap();
1963        assert!(fd_table.is_closed(4));
1964        assert!(!fd_table.is_open(4));
1965
1966        // fd 3 and 5 should still be open
1967        assert!(fd_table.is_open(3));
1968        assert!(fd_table.is_open(5));
1969
1970        // Clean up
1971        let _ = std::fs::remove_file(temp_file1);
1972        let _ = std::fs::remove_file(temp_file2);
1973    }
1974
1975    #[test]
1976    fn test_shell_state_has_fd_table() {
1977        let state = ShellState::new();
1978        let fd_table = state.fd_table.borrow();
1979        assert!(!fd_table.is_open(3));
1980    }
1981
1982    #[test]
1983    fn test_shell_state_fd_table_operations() {
1984        let state = ShellState::new();
1985
1986        // Create a temporary file
1987        let temp_file = "/tmp/rush_test_state_fd.txt";
1988        std::fs::write(temp_file, "test content").unwrap();
1989
1990        // Open file through shell state's fd table
1991        {
1992            let mut fd_table = state.fd_table.borrow_mut();
1993            fd_table
1994                .open_fd(3, temp_file, true, false, false, false, false)
1995                .unwrap();
1996        }
1997
1998        // Verify it's open
1999        {
2000            let fd_table = state.fd_table.borrow();
2001            assert!(fd_table.is_open(3));
2002        }
2003
2004        // Clean up
2005        let _ = std::fs::remove_file(temp_file);
2006    }
2007
2008    // ShellOptions Tests
2009
2010    #[test]
2011    fn test_shell_options_default() {
2012        let options = ShellOptions::default();
2013        assert!(!options.errexit);
2014        assert!(!options.nounset);
2015        assert!(!options.xtrace);
2016        assert!(!options.verbose);
2017        assert!(!options.noexec);
2018        assert!(!options.noglob);
2019        assert!(!options.noclobber);
2020        assert!(!options.allexport);
2021        assert!(!options.notify);
2022        assert!(!options.ignoreeof);
2023        assert!(!options.monitor);
2024    }
2025
2026    #[test]
2027    fn test_shell_options_get_by_short_name() {
2028        let mut options = ShellOptions::default();
2029        options.errexit = true;
2030        options.nounset = true;
2031
2032        assert_eq!(options.get_by_short_name('e'), Some(true));
2033        assert_eq!(options.get_by_short_name('u'), Some(true));
2034        assert_eq!(options.get_by_short_name('x'), Some(false));
2035        assert_eq!(options.get_by_short_name('Z'), None);
2036    }
2037
2038    #[test]
2039    fn test_shell_options_set_by_short_name() {
2040        let mut options = ShellOptions::default();
2041
2042        assert!(options.set_by_short_name('e', true).is_ok());
2043        assert!(options.errexit);
2044
2045        assert!(options.set_by_short_name('u', true).is_ok());
2046        assert!(options.nounset);
2047
2048        assert!(options.set_by_short_name('x', true).is_ok());
2049        assert!(options.xtrace);
2050
2051        assert!(options.set_by_short_name('e', false).is_ok());
2052        assert!(!options.errexit);
2053
2054        // Invalid option
2055        assert!(options.set_by_short_name('Z', true).is_err());
2056    }
2057
2058    #[test]
2059    fn test_shell_options_get_by_long_name() {
2060        let mut options = ShellOptions::default();
2061        options.errexit = true;
2062        options.nounset = true;
2063
2064        assert_eq!(options.get_by_long_name("errexit"), Some(true));
2065        assert_eq!(options.get_by_long_name("nounset"), Some(true));
2066        assert_eq!(options.get_by_long_name("xtrace"), Some(false));
2067        assert_eq!(options.get_by_long_name("invalid"), None);
2068    }
2069
2070    #[test]
2071    fn test_shell_options_set_by_long_name() {
2072        let mut options = ShellOptions::default();
2073
2074        assert!(options.set_by_long_name("errexit", true).is_ok());
2075        assert!(options.errexit);
2076
2077        assert!(options.set_by_long_name("nounset", true).is_ok());
2078        assert!(options.nounset);
2079
2080        assert!(options.set_by_long_name("xtrace", true).is_ok());
2081        assert!(options.xtrace);
2082
2083        assert!(options.set_by_long_name("errexit", false).is_ok());
2084        assert!(!options.errexit);
2085
2086        // Invalid option
2087        assert!(options.set_by_long_name("invalid", true).is_err());
2088    }
2089
2090    #[test]
2091    fn test_shell_options_all_short_options() {
2092        let mut options = ShellOptions::default();
2093
2094        // Test all valid short options
2095        let short_opts = vec!['e', 'u', 'x', 'v', 'n', 'f', 'C', 'a', 'b', 'm'];
2096        for opt in short_opts {
2097            assert!(options.set_by_short_name(opt, true).is_ok());
2098            assert_eq!(options.get_by_short_name(opt), Some(true));
2099            assert!(options.set_by_short_name(opt, false).is_ok());
2100            assert_eq!(options.get_by_short_name(opt), Some(false));
2101        }
2102    }
2103
2104    #[test]
2105    fn test_shell_options_all_long_options() {
2106        let mut options = ShellOptions::default();
2107
2108        // Test all valid long options
2109        let long_opts = vec![
2110            "errexit",
2111            "nounset",
2112            "xtrace",
2113            "verbose",
2114            "noexec",
2115            "noglob",
2116            "noclobber",
2117            "allexport",
2118            "notify",
2119            "ignoreeof",
2120            "monitor",
2121        ];
2122        for opt in long_opts {
2123            assert!(options.set_by_long_name(opt, true).is_ok());
2124            assert_eq!(options.get_by_long_name(opt), Some(true));
2125            assert!(options.set_by_long_name(opt, false).is_ok());
2126            assert_eq!(options.get_by_long_name(opt), Some(false));
2127        }
2128    }
2129
2130    #[test]
2131    fn test_shell_options_get_all_options() {
2132        let mut options = ShellOptions::default();
2133        options.errexit = true;
2134        options.xtrace = true;
2135
2136        let all_options = options.get_all_options();
2137
2138        // Should have 11 options
2139        assert_eq!(all_options.len(), 11);
2140
2141        // Find errexit and verify it's on
2142        let errexit_opt = all_options.iter().find(|(name, _, _)| *name == "errexit");
2143        assert!(errexit_opt.is_some());
2144        assert_eq!(errexit_opt.unwrap().2, true);
2145
2146        // Find xtrace and verify it's on
2147        let xtrace_opt = all_options.iter().find(|(name, _, _)| *name == "xtrace");
2148        assert!(xtrace_opt.is_some());
2149        assert_eq!(xtrace_opt.unwrap().2, true);
2150
2151        // Find nounset and verify it's off
2152        let nounset_opt = all_options.iter().find(|(name, _, _)| *name == "nounset");
2153        assert!(nounset_opt.is_some());
2154        assert_eq!(nounset_opt.unwrap().2, false);
2155    }
2156
2157    #[test]
2158    fn test_shell_state_has_options() {
2159        let state = ShellState::new();
2160        assert!(!state.options.errexit);
2161        assert!(!state.options.nounset);
2162        assert!(!state.options.xtrace);
2163    }
2164
2165    #[test]
2166    fn test_shell_state_options_modification() {
2167        let mut state = ShellState::new();
2168
2169        state.options.errexit = true;
2170        assert!(state.options.errexit);
2171
2172        state.options.set_by_short_name('u', true).unwrap();
2173        assert!(state.options.nounset);
2174
2175        state.options.set_by_long_name("xtrace", true).unwrap();
2176        assert!(state.options.xtrace);
2177    }
2178
2179    #[test]
2180    fn test_shell_options_error_messages() {
2181        let mut options = ShellOptions::default();
2182
2183        let result = options.set_by_short_name('Z', true);
2184        assert!(result.is_err());
2185        assert!(result.unwrap_err().contains("Invalid option: -Z"));
2186
2187        let result = options.set_by_long_name("invalid_option", true);
2188        assert!(result.is_err());
2189        assert!(
2190            result
2191                .unwrap_err()
2192                .contains("Invalid option: invalid_option")
2193        );
2194    }
2195
2196    #[test]
2197    fn test_shell_options_case_sensitivity() {
2198        let mut options = ShellOptions::default();
2199
2200        // 'C' is valid (noclobber), 'c' is not
2201        assert!(options.set_by_short_name('C', true).is_ok());
2202        assert!(options.noclobber);
2203        assert!(options.set_by_short_name('c', true).is_err());
2204    }
2205}