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