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