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::io::IsTerminal;
7use std::rc::Rc;
8use std::sync::{Arc, Mutex};
9use std::time::Instant;
10
11lazy_static! {
12    /// Global queue for pending signal events
13    /// Signals are enqueued by the signal handler thread and dequeued by the main thread
14    pub static ref SIGNAL_QUEUE: Arc<Mutex<VecDeque<SignalEvent>>> =
15        Arc::new(Mutex::new(VecDeque::new()));
16}
17
18/// Maximum number of signals to queue before dropping old ones
19const MAX_SIGNAL_QUEUE_SIZE: usize = 100;
20
21/// Represents a signal event that needs to be processed
22#[derive(Debug, Clone)]
23pub struct SignalEvent {
24    /// Signal name (e.g., "INT", "TERM")
25    pub signal_name: String,
26    /// Signal number (e.g., 2, 15)
27    #[allow(dead_code)]
28    pub signal_number: i32,
29    /// When the signal was received
30    #[allow(dead_code)]
31    pub timestamp: Instant,
32}
33
34impl SignalEvent {
35    pub fn new(signal_name: String, signal_number: i32) -> Self {
36        Self {
37            signal_name,
38            signal_number,
39            timestamp: Instant::now(),
40        }
41    }
42}
43
44#[derive(Debug, Clone)]
45pub struct ColorScheme {
46    /// ANSI color code for prompt
47    pub prompt: String,
48    /// ANSI color code for error messages
49    pub error: String,
50    /// ANSI color code for success messages
51    pub success: String,
52    /// ANSI color code for builtin command output
53    pub builtin: String,
54    /// ANSI color code for directory listings
55    pub directory: String,
56}
57
58impl Default for ColorScheme {
59    fn default() -> Self {
60        Self {
61            prompt: "\x1b[32m".to_string(),    // Green
62            error: "\x1b[31m".to_string(),     // Red
63            success: "\x1b[32m".to_string(),   // Green
64            builtin: "\x1b[36m".to_string(),   // Cyan
65            directory: "\x1b[34m".to_string(), // Blue
66        }
67    }
68}
69
70#[derive(Debug, Clone)]
71pub struct ShellState {
72    /// Shell variables (local to the shell session)
73    pub variables: HashMap<String, String>,
74    /// Which variables are exported to child processes
75    pub exported: HashSet<String>,
76    /// Last exit code ($?)
77    pub last_exit_code: i32,
78    /// Shell process ID ($$)
79    pub shell_pid: u32,
80    /// Script name or command ($0)
81    pub script_name: String,
82    /// Directory stack for pushd/popd
83    pub dir_stack: Vec<String>,
84    /// Command aliases
85    pub aliases: HashMap<String, String>,
86    /// Whether colors are enabled
87    pub colors_enabled: bool,
88    /// Current color scheme
89    pub color_scheme: ColorScheme,
90    /// Positional parameters ($1, $2, $3, ...)
91    pub positional_params: Vec<String>,
92    /// Function definitions
93    pub functions: HashMap<String, Ast>,
94    /// Local variable stack for function scoping
95    pub local_vars: Vec<HashMap<String, String>>,
96    /// Function call depth for local scope management
97    pub function_depth: usize,
98    /// Maximum allowed recursion depth
99    pub max_recursion_depth: usize,
100    /// Flag to indicate if we're currently returning from a function
101    pub returning: bool,
102    /// Return value when returning from a function
103    pub return_value: Option<i32>,
104    /// Output capture buffer for command substitution
105    pub capture_output: Option<Rc<RefCell<Vec<u8>>>>,
106    /// Whether to use condensed cwd display in prompt
107    pub condensed_cwd: bool,
108    /// Signal trap handlers: maps signal name to command string
109    pub trap_handlers: Arc<Mutex<HashMap<String, String>>>,
110    /// Flag to track if EXIT trap has been executed
111    pub exit_trap_executed: bool,
112    /// Flag to indicate that the shell should exit
113    pub exit_requested: bool,
114    /// Exit code to use when exiting
115    pub exit_code: i32,
116    /// Flag to indicate pending signals need processing
117    /// Set by signal handler, checked by executor
118    #[allow(dead_code)]
119    pub pending_signals: bool,
120    /// Pending here-document content from script execution
121    pub pending_heredoc_content: Option<String>,
122    /// Interactive mode heredoc collection state
123    pub collecting_heredoc: Option<(String, String, String)>, // (command_line, delimiter, collected_content)
124}
125
126impl ShellState {
127    pub fn new() -> Self {
128        let shell_pid = std::process::id();
129
130        // Check NO_COLOR environment variable (respects standard)
131        let no_color = env::var("NO_COLOR").is_ok();
132
133        // Check RUSH_COLORS environment variable for explicit control
134        let rush_colors = env::var("RUSH_COLORS")
135            .map(|v| v.to_lowercase())
136            .unwrap_or_else(|_| "auto".to_string());
137
138        let colors_enabled = match rush_colors.as_str() {
139            "1" | "true" | "on" | "enable" => !no_color && std::io::stdout().is_terminal(),
140            "0" | "false" | "off" | "disable" => false,
141            "auto" => !no_color && std::io::stdout().is_terminal(),
142            _ => !no_color && std::io::stdout().is_terminal(),
143        };
144
145        // Check RUSH_CONDENSED environment variable for cwd display preference
146        let rush_condensed = env::var("RUSH_CONDENSED")
147            .map(|v| v.to_lowercase())
148            .unwrap_or_else(|_| "true".to_string());
149
150        let condensed_cwd = match rush_condensed.as_str() {
151            "1" | "true" | "on" | "enable" => true,
152            "0" | "false" | "off" | "disable" => false,
153            _ => true, // Default to condensed for backward compatibility
154        };
155
156        Self {
157            variables: HashMap::new(),
158            exported: HashSet::new(),
159            last_exit_code: 0,
160            shell_pid,
161            script_name: "rush".to_string(),
162            dir_stack: Vec::new(),
163            aliases: HashMap::new(),
164            colors_enabled,
165            color_scheme: ColorScheme::default(),
166            positional_params: Vec::new(),
167            functions: HashMap::new(),
168            local_vars: Vec::new(),
169            function_depth: 0,
170            max_recursion_depth: 500, // Default recursion limit (reduced to avoid Rust stack overflow)
171            returning: false,
172            return_value: None,
173            capture_output: None,
174            condensed_cwd,
175            trap_handlers: Arc::new(Mutex::new(HashMap::new())),
176            exit_trap_executed: false,
177            exit_requested: false,
178            exit_code: 0,
179            pending_signals: false,
180            pending_heredoc_content: None,
181            collecting_heredoc: None,
182        }
183    }
184
185    /// Get a variable value, checking local scopes first, then shell variables, then environment
186    pub fn get_var(&self, name: &str) -> Option<String> {
187        // Handle special variables (these are never local)
188        match name {
189            "?" => Some(self.last_exit_code.to_string()),
190            "$" => Some(self.shell_pid.to_string()),
191            "0" => Some(self.script_name.clone()),
192            "*" => {
193                // $* - all positional parameters as single string (space-separated)
194                if self.positional_params.is_empty() {
195                    Some("".to_string())
196                } else {
197                    Some(self.positional_params.join(" "))
198                }
199            }
200            "@" => {
201                // $@ - all positional parameters as separate words (but returns as single string for compatibility)
202                if self.positional_params.is_empty() {
203                    Some("".to_string())
204                } else {
205                    Some(self.positional_params.join(" "))
206                }
207            }
208            "#" => Some(self.positional_params.len().to_string()),
209            _ => {
210                // Handle positional parameters $1, $2, $3, etc. (these are never local)
211                if let Ok(index) = name.parse::<usize>()
212                    && index > 0
213                    && index <= self.positional_params.len()
214                {
215                    return Some(self.positional_params[index - 1].clone());
216                }
217
218                // Check local scopes first, then shell variables, then environment
219                // Search local scopes from innermost to outermost
220                for scope in self.local_vars.iter().rev() {
221                    if let Some(value) = scope.get(name) {
222                        return Some(value.clone());
223                    }
224                }
225
226                // Check shell variables
227                if let Some(value) = self.variables.get(name) {
228                    Some(value.clone())
229                } else {
230                    // Fall back to environment variables
231                    env::var(name).ok()
232                }
233            }
234        }
235    }
236
237    /// Set a shell variable (updates local scope if variable exists there, otherwise sets globally)
238    pub fn set_var(&mut self, name: &str, value: String) {
239        // Check if this variable exists in any local scope
240        // If it does, update it there instead of setting globally
241        for scope in self.local_vars.iter_mut().rev() {
242            if scope.contains_key(name) {
243                scope.insert(name.to_string(), value);
244                return;
245            }
246        }
247
248        // Variable doesn't exist in local scopes, set it globally
249        self.variables.insert(name.to_string(), value);
250    }
251
252    /// Remove a shell variable
253    pub fn unset_var(&mut self, name: &str) {
254        self.variables.remove(name);
255        self.exported.remove(name);
256    }
257
258    /// Mark a variable as exported
259    pub fn export_var(&mut self, name: &str) {
260        if self.variables.contains_key(name) {
261            self.exported.insert(name.to_string());
262        }
263    }
264
265    /// Set and export a variable
266    pub fn set_exported_var(&mut self, name: &str, value: String) {
267        self.set_var(name, value);
268        self.export_var(name);
269    }
270
271    /// Get all environment variables for child processes (exported + inherited)
272    pub fn get_env_for_child(&self) -> HashMap<String, String> {
273        let mut child_env = HashMap::new();
274
275        // Add all current environment variables
276        for (key, value) in env::vars() {
277            child_env.insert(key, value);
278        }
279
280        // Override with exported shell variables
281        for var_name in &self.exported {
282            if let Some(value) = self.variables.get(var_name) {
283                child_env.insert(var_name.clone(), value.clone());
284            }
285        }
286
287        child_env
288    }
289
290    /// Update the last exit code
291    pub fn set_last_exit_code(&mut self, code: i32) {
292        self.last_exit_code = code;
293    }
294
295    /// Set the script name ($0)
296    pub fn set_script_name(&mut self, name: &str) {
297        self.script_name = name.to_string();
298    }
299
300    /// Get the condensed current working directory for the prompt
301    pub fn get_condensed_cwd(&self) -> String {
302        match env::current_dir() {
303            Ok(path) => {
304                let path_str = path.to_string_lossy();
305                let components: Vec<&str> = path_str.split('/').collect();
306                if components.is_empty() || (components.len() == 1 && components[0].is_empty()) {
307                    return "/".to_string();
308                }
309                let mut result = String::new();
310                for (i, comp) in components.iter().enumerate() {
311                    if comp.is_empty() {
312                        continue; // skip leading empty component
313                    }
314                    if i == components.len() - 1 {
315                        result.push('/');
316                        result.push_str(comp);
317                    } else {
318                        result.push('/');
319                        if let Some(first) = comp.chars().next() {
320                            result.push(first);
321                        }
322                    }
323                }
324                if result.is_empty() {
325                    "/".to_string()
326                } else {
327                    result
328                }
329            }
330            Err(_) => "/?".to_string(), // fallback if can't get cwd
331        }
332    }
333
334    /// Get the full current working directory for the prompt
335    pub fn get_full_cwd(&self) -> String {
336        match env::current_dir() {
337            Ok(path) => path.to_string_lossy().to_string(),
338            Err(_) => "/?".to_string(), // fallback if can't get cwd
339        }
340    }
341
342    /// Get the user@hostname string for the prompt
343    pub fn get_user_hostname(&self) -> String {
344        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
345
346        // First try to get hostname from HOSTNAME environment variable
347        if let Ok(hostname) = env::var("HOSTNAME")
348            && !hostname.trim().is_empty()
349        {
350            return format!("{}@{}", user, hostname);
351        }
352
353        // If HOSTNAME is not set or empty, try the hostname command
354        let hostname = match std::process::Command::new("hostname").output() {
355            Ok(output) if output.status.success() => {
356                String::from_utf8_lossy(&output.stdout).trim().to_string()
357            }
358            _ => "hostname".to_string(), // Last resort fallback
359        };
360
361        // Set the HOSTNAME environment variable for future use
362        if hostname != "hostname" {
363            unsafe {
364                env::set_var("HOSTNAME", &hostname);
365            }
366        }
367
368        format!("{}@{}", user, hostname)
369    }
370
371    /// Get the full prompt string
372    pub fn get_prompt(&self) -> String {
373        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
374        let prompt_char = if user == "root" { "#" } else { "$" };
375        let cwd = if self.condensed_cwd {
376            self.get_condensed_cwd()
377        } else {
378            self.get_full_cwd()
379        };
380        format!("{}:{} {} ", self.get_user_hostname(), cwd, prompt_char)
381    }
382
383    /// Set an alias
384    pub fn set_alias(&mut self, name: &str, value: String) {
385        self.aliases.insert(name.to_string(), value);
386    }
387
388    /// Get an alias value
389    pub fn get_alias(&self, name: &str) -> Option<&String> {
390        self.aliases.get(name)
391    }
392
393    /// Remove an alias
394    pub fn remove_alias(&mut self, name: &str) {
395        self.aliases.remove(name);
396    }
397
398    /// Get all aliases
399    pub fn get_all_aliases(&self) -> &HashMap<String, String> {
400        &self.aliases
401    }
402
403    /// Set positional parameters
404    pub fn set_positional_params(&mut self, params: Vec<String>) {
405        self.positional_params = params;
406    }
407
408    /// Get positional parameters
409    #[allow(dead_code)]
410    pub fn get_positional_params(&self) -> &[String] {
411        &self.positional_params
412    }
413
414    /// Shift positional parameters (remove first n parameters)
415    pub fn shift_positional_params(&mut self, count: usize) {
416        if count > 0 {
417            for _ in 0..count {
418                if !self.positional_params.is_empty() {
419                    self.positional_params.remove(0);
420                }
421            }
422        }
423    }
424
425    /// Add a positional parameter at the end
426    #[allow(dead_code)]
427    pub fn push_positional_param(&mut self, param: String) {
428        self.positional_params.push(param);
429    }
430
431    /// Define a function
432    pub fn define_function(&mut self, name: String, body: Ast) {
433        self.functions.insert(name, body);
434    }
435
436    /// Get a function definition
437    pub fn get_function(&self, name: &str) -> Option<&Ast> {
438        self.functions.get(name)
439    }
440
441    /// Remove a function definition
442    #[allow(dead_code)]
443    pub fn remove_function(&mut self, name: &str) {
444        self.functions.remove(name);
445    }
446
447    /// Get all function names
448    #[allow(dead_code)]
449    pub fn get_function_names(&self) -> Vec<&String> {
450        self.functions.keys().collect()
451    }
452
453    /// Push a new local variable scope
454    pub fn push_local_scope(&mut self) {
455        self.local_vars.push(HashMap::new());
456    }
457
458    /// Pop the current local variable scope
459    pub fn pop_local_scope(&mut self) {
460        if !self.local_vars.is_empty() {
461            self.local_vars.pop();
462        }
463    }
464
465    /// Set a local variable in the current scope
466    pub fn set_local_var(&mut self, name: &str, value: String) {
467        if let Some(current_scope) = self.local_vars.last_mut() {
468            current_scope.insert(name.to_string(), value);
469        } else {
470            // If no local scope exists, set as global variable
471            self.set_var(name, value);
472        }
473    }
474
475    /// Enter a function context (push local scope if needed)
476    pub fn enter_function(&mut self) {
477        self.function_depth += 1;
478        if self.function_depth > self.local_vars.len() {
479            self.push_local_scope();
480        }
481    }
482
483    /// Exit a function context (pop local scope if needed)
484    pub fn exit_function(&mut self) {
485        if self.function_depth > 0 {
486            self.function_depth -= 1;
487            if self.function_depth == self.local_vars.len() - 1 {
488                self.pop_local_scope();
489            }
490        }
491    }
492
493    /// Set return state for function returns
494    pub fn set_return(&mut self, value: i32) {
495        self.returning = true;
496        self.return_value = Some(value);
497    }
498
499    /// Clear return state
500    pub fn clear_return(&mut self) {
501        self.returning = false;
502        self.return_value = None;
503    }
504
505    /// Check if currently returning
506    pub fn is_returning(&self) -> bool {
507        self.returning
508    }
509
510    /// Get return value if returning
511    pub fn get_return_value(&self) -> Option<i32> {
512        self.return_value
513    }
514
515    /// Set a trap handler for a signal
516    pub fn set_trap(&mut self, signal: &str, command: String) {
517        if let Ok(mut handlers) = self.trap_handlers.lock() {
518            handlers.insert(signal.to_uppercase(), command);
519        }
520    }
521
522    /// Get a trap handler for a signal
523    pub fn get_trap(&self, signal: &str) -> Option<String> {
524        if let Ok(handlers) = self.trap_handlers.lock() {
525            handlers.get(&signal.to_uppercase()).cloned()
526        } else {
527            None
528        }
529    }
530
531    /// Remove a trap handler for a signal
532    pub fn remove_trap(&mut self, signal: &str) {
533        if let Ok(mut handlers) = self.trap_handlers.lock() {
534            handlers.remove(&signal.to_uppercase());
535        }
536    }
537
538    /// Get all trap handlers
539    pub fn get_all_traps(&self) -> HashMap<String, String> {
540        if let Ok(handlers) = self.trap_handlers.lock() {
541            handlers.clone()
542        } else {
543            HashMap::new()
544        }
545    }
546
547    /// Clear all trap handlers
548    #[allow(dead_code)]
549    pub fn clear_traps(&mut self) {
550        if let Ok(mut handlers) = self.trap_handlers.lock() {
551            handlers.clear();
552        }
553    }
554}
555
556/// Enqueue a signal event for later processing
557/// If the queue is full, the oldest event is dropped
558pub fn enqueue_signal(signal_name: &str, signal_number: i32) {
559    if let Ok(mut queue) = SIGNAL_QUEUE.lock() {
560        // If queue is full, remove oldest event
561        if queue.len() >= MAX_SIGNAL_QUEUE_SIZE {
562            queue.pop_front();
563            eprintln!("Warning: Signal queue overflow, dropping oldest signal");
564        }
565
566        queue.push_back(SignalEvent::new(signal_name.to_string(), signal_number));
567    }
568}
569
570/// Process all pending signals in the queue
571/// This should be called at safe points during command execution
572pub fn process_pending_signals(shell_state: &mut ShellState) {
573    // Try to lock the queue with a timeout to avoid blocking
574    if let Ok(mut queue) = SIGNAL_QUEUE.lock() {
575        // Process all pending signals
576        while let Some(signal_event) = queue.pop_front() {
577            // Check if a trap is set for this signal
578            if let Some(trap_cmd) = shell_state.get_trap(&signal_event.signal_name)
579                && !trap_cmd.is_empty()
580            {
581                // Execute the trap handler
582                // Note: This preserves the exit code as per POSIX requirements
583                crate::executor::execute_trap_handler(&trap_cmd, shell_state);
584            }
585        }
586    }
587}
588
589impl Default for ShellState {
590    fn default() -> Self {
591        Self::new()
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598
599    #[test]
600    fn test_shell_state_basic() {
601        let mut state = ShellState::new();
602        state.set_var("TEST_VAR", "test_value".to_string());
603        assert_eq!(state.get_var("TEST_VAR"), Some("test_value".to_string()));
604    }
605
606    #[test]
607    fn test_special_variables() {
608        let mut state = ShellState::new();
609        state.set_last_exit_code(42);
610        state.set_script_name("test_script");
611
612        assert_eq!(state.get_var("?"), Some("42".to_string()));
613        assert_eq!(state.get_var("$"), Some(state.shell_pid.to_string()));
614        assert_eq!(state.get_var("0"), Some("test_script".to_string()));
615    }
616
617    #[test]
618    fn test_export_variable() {
619        let mut state = ShellState::new();
620        state.set_var("EXPORT_VAR", "export_value".to_string());
621        state.export_var("EXPORT_VAR");
622
623        let child_env = state.get_env_for_child();
624        assert_eq!(
625            child_env.get("EXPORT_VAR"),
626            Some(&"export_value".to_string())
627        );
628    }
629
630    #[test]
631    fn test_unset_variable() {
632        let mut state = ShellState::new();
633        state.set_var("UNSET_VAR", "value".to_string());
634        state.export_var("UNSET_VAR");
635
636        assert!(state.variables.contains_key("UNSET_VAR"));
637        assert!(state.exported.contains("UNSET_VAR"));
638
639        state.unset_var("UNSET_VAR");
640
641        assert!(!state.variables.contains_key("UNSET_VAR"));
642        assert!(!state.exported.contains("UNSET_VAR"));
643    }
644
645    #[test]
646    fn test_get_user_hostname() {
647        let state = ShellState::new();
648        let user_hostname = state.get_user_hostname();
649        // Should contain @ since it's user@hostname format
650        assert!(user_hostname.contains('@'));
651    }
652
653    #[test]
654    fn test_get_prompt() {
655        let state = ShellState::new();
656        let prompt = state.get_prompt();
657        // Should end with $ and contain @
658        assert!(prompt.ends_with(" $ "));
659        assert!(prompt.contains('@'));
660    }
661
662    #[test]
663    fn test_positional_parameters() {
664        let mut state = ShellState::new();
665        state.set_positional_params(vec![
666            "arg1".to_string(),
667            "arg2".to_string(),
668            "arg3".to_string(),
669        ]);
670
671        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
672        assert_eq!(state.get_var("2"), Some("arg2".to_string()));
673        assert_eq!(state.get_var("3"), Some("arg3".to_string()));
674        assert_eq!(state.get_var("4"), None);
675        assert_eq!(state.get_var("#"), Some("3".to_string()));
676        assert_eq!(state.get_var("*"), Some("arg1 arg2 arg3".to_string()));
677        assert_eq!(state.get_var("@"), Some("arg1 arg2 arg3".to_string()));
678    }
679
680    #[test]
681    fn test_positional_parameters_empty() {
682        let mut state = ShellState::new();
683        state.set_positional_params(vec![]);
684
685        assert_eq!(state.get_var("1"), None);
686        assert_eq!(state.get_var("#"), Some("0".to_string()));
687        assert_eq!(state.get_var("*"), Some("".to_string()));
688        assert_eq!(state.get_var("@"), Some("".to_string()));
689    }
690
691    #[test]
692    fn test_shift_positional_params() {
693        let mut state = ShellState::new();
694        state.set_positional_params(vec![
695            "arg1".to_string(),
696            "arg2".to_string(),
697            "arg3".to_string(),
698        ]);
699
700        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
701        assert_eq!(state.get_var("2"), Some("arg2".to_string()));
702        assert_eq!(state.get_var("3"), Some("arg3".to_string()));
703
704        state.shift_positional_params(1);
705
706        assert_eq!(state.get_var("1"), Some("arg2".to_string()));
707        assert_eq!(state.get_var("2"), Some("arg3".to_string()));
708        assert_eq!(state.get_var("3"), None);
709        assert_eq!(state.get_var("#"), Some("2".to_string()));
710
711        state.shift_positional_params(2);
712
713        assert_eq!(state.get_var("1"), None);
714        assert_eq!(state.get_var("#"), Some("0".to_string()));
715    }
716
717    #[test]
718    fn test_push_positional_param() {
719        let mut state = ShellState::new();
720        state.set_positional_params(vec!["arg1".to_string()]);
721
722        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
723        assert_eq!(state.get_var("#"), Some("1".to_string()));
724
725        state.push_positional_param("arg2".to_string());
726
727        assert_eq!(state.get_var("1"), Some("arg1".to_string()));
728        assert_eq!(state.get_var("2"), Some("arg2".to_string()));
729        assert_eq!(state.get_var("#"), Some("2".to_string()));
730    }
731
732    #[test]
733    fn test_local_variable_scoping() {
734        let mut state = ShellState::new();
735
736        // Set a global variable
737        state.set_var("global_var", "global_value".to_string());
738        assert_eq!(
739            state.get_var("global_var"),
740            Some("global_value".to_string())
741        );
742
743        // Push local scope
744        state.push_local_scope();
745
746        // Set a local variable with the same name
747        state.set_local_var("global_var", "local_value".to_string());
748        assert_eq!(state.get_var("global_var"), Some("local_value".to_string()));
749
750        // Set another local variable
751        state.set_local_var("local_var", "local_only".to_string());
752        assert_eq!(state.get_var("local_var"), Some("local_only".to_string()));
753
754        // Pop local scope
755        state.pop_local_scope();
756
757        // Should be back to global variable
758        assert_eq!(
759            state.get_var("global_var"),
760            Some("global_value".to_string())
761        );
762        assert_eq!(state.get_var("local_var"), None);
763    }
764
765    #[test]
766    fn test_nested_local_scopes() {
767        let mut state = ShellState::new();
768
769        // Set global variable
770        state.set_var("test_var", "global".to_string());
771
772        // Push first local scope
773        state.push_local_scope();
774        state.set_local_var("test_var", "level1".to_string());
775        assert_eq!(state.get_var("test_var"), Some("level1".to_string()));
776
777        // Push second local scope
778        state.push_local_scope();
779        state.set_local_var("test_var", "level2".to_string());
780        assert_eq!(state.get_var("test_var"), Some("level2".to_string()));
781
782        // Pop second scope
783        state.pop_local_scope();
784        assert_eq!(state.get_var("test_var"), Some("level1".to_string()));
785
786        // Pop first scope
787        state.pop_local_scope();
788        assert_eq!(state.get_var("test_var"), Some("global".to_string()));
789    }
790
791    #[test]
792    fn test_variable_set_in_local_scope() {
793        let mut state = ShellState::new();
794
795        // No local scope initially
796        state.set_var("test_var", "global".to_string());
797        assert_eq!(state.get_var("test_var"), Some("global".to_string()));
798
799        // Push local scope and set local variable
800        state.push_local_scope();
801        state.set_local_var("test_var", "local".to_string());
802        assert_eq!(state.get_var("test_var"), Some("local".to_string()));
803
804        // Pop scope
805        state.pop_local_scope();
806        assert_eq!(state.get_var("test_var"), Some("global".to_string()));
807    }
808
809    #[test]
810    fn test_condensed_cwd_environment_variable() {
811        // Test default behavior (should be true for backward compatibility)
812        let state = ShellState::new();
813        assert!(state.condensed_cwd);
814
815        // Test explicit true
816        unsafe {
817            env::set_var("RUSH_CONDENSED", "true");
818        }
819        let state = ShellState::new();
820        assert!(state.condensed_cwd);
821
822        // Test explicit false
823        unsafe {
824            env::set_var("RUSH_CONDENSED", "false");
825        }
826        let state = ShellState::new();
827        assert!(!state.condensed_cwd);
828
829        // Clean up
830        unsafe {
831            env::remove_var("RUSH_CONDENSED");
832        }
833    }
834
835    #[test]
836    fn test_get_full_cwd() {
837        let state = ShellState::new();
838        let full_cwd = state.get_full_cwd();
839        assert!(!full_cwd.is_empty());
840        // Should contain path separators (either / or \ depending on platform)
841        assert!(full_cwd.contains('/') || full_cwd.contains('\\'));
842    }
843
844    #[test]
845    fn test_prompt_with_condensed_setting() {
846        let mut state = ShellState::new();
847
848        // Test with condensed enabled (default)
849        assert!(state.condensed_cwd);
850        let prompt_condensed = state.get_prompt();
851        assert!(prompt_condensed.contains('@'));
852
853        // Test with condensed disabled
854        state.condensed_cwd = false;
855        let prompt_full = state.get_prompt();
856        assert!(prompt_full.contains('@'));
857
858        // Both should end with "$ " (or "# " for root)
859        assert!(prompt_condensed.ends_with("$ ") || prompt_condensed.ends_with("# "));
860        assert!(prompt_full.ends_with("$ ") || prompt_full.ends_with("# "));
861    }
862}