rush_sh/state/
mod.rs

1// File descriptor table module
2pub mod fd_table;
3
4// Shell options module
5pub mod options;
6
7// Signal management module
8pub mod signals;
9
10// Re-export types for backward compatibility
11pub use fd_table::FileDescriptorTable;
12pub use options::ShellOptions;
13pub use signals::{enqueue_signal, process_pending_signals};
14
15use super::parser::Ast;
16use std::cell::RefCell;
17use std::collections::{HashMap, HashSet};
18use std::env;
19use std::io::IsTerminal;
20use std::os::unix::io::RawFd;
21use std::rc::Rc;
22use std::sync::{Arc, Mutex};
23
24#[derive(Debug, Clone)]
25pub struct ColorScheme {
26    /// ANSI color code for prompt
27    pub prompt: String,
28    /// ANSI color code for error messages
29    pub error: String,
30    /// ANSI color code for success messages
31    pub success: String,
32    /// ANSI color code for builtin command output
33    pub builtin: String,
34    /// ANSI color code for directory listings
35    pub directory: String,
36}
37
38impl Default for ColorScheme {
39    fn default() -> Self {
40        Self {
41            prompt: "\x1b[32m".to_string(),    // Green
42            error: "\x1b[31m".to_string(),     // Red
43            success: "\x1b[32m".to_string(),   // Green
44            builtin: "\x1b[36m".to_string(),   // Cyan
45            directory: "\x1b[34m".to_string(), // Blue
46        }
47    }
48}
49
50#[derive(Debug, Clone)]
51pub struct ShellState {
52    /// Shell variables (local to the shell session)
53    pub variables: HashMap<String, String>,
54    /// Which variables are exported to child processes
55    pub exported: HashSet<String>,
56    /// Last exit code ($?)
57    pub last_exit_code: i32,
58    /// Shell process ID ($$)
59    pub shell_pid: u32,
60    /// Script name or command ($0)
61    pub script_name: String,
62    /// Directory stack for pushd/popd
63    pub dir_stack: Vec<String>,
64    /// Command aliases
65    pub aliases: HashMap<String, String>,
66    /// Whether colors are enabled
67    pub colors_enabled: bool,
68    /// Current color scheme
69    pub color_scheme: ColorScheme,
70    /// Positional parameters ($1, $2, $3, ...)
71    pub positional_params: Vec<String>,
72    /// Function definitions
73    pub functions: HashMap<String, Ast>,
74    /// Local variable stack for function scoping
75    pub local_vars: Vec<HashMap<String, String>>,
76    /// Function call depth for local scope management
77    pub function_depth: usize,
78    /// Maximum allowed recursion depth
79    pub max_recursion_depth: usize,
80    /// Flag to indicate if we're currently returning from a function
81    pub returning: bool,
82    /// Return value when returning from a function
83    pub return_value: Option<i32>,
84    /// Loop nesting depth for break/continue
85    pub loop_depth: usize,
86    /// Flag to indicate if we're breaking out of a loop
87    pub breaking: bool,
88    /// Number of loop levels to break out of
89    pub break_level: usize,
90    /// Flag to indicate if we're continuing to next loop iteration
91    pub continuing: bool,
92    /// Number of loop levels to continue from
93    pub continue_level: usize,
94    /// Output capture buffer for command substitution
95    pub capture_output: Option<Rc<RefCell<Vec<u8>>>>,
96    /// Whether to use condensed cwd display in prompt
97    pub condensed_cwd: bool,
98    /// Signal trap handlers: maps signal name to command string
99    pub trap_handlers: Arc<Mutex<HashMap<String, String>>>,
100    /// Flag to track if EXIT trap has been executed
101    pub exit_trap_executed: bool,
102    /// Flag to indicate that the shell should exit
103    pub exit_requested: bool,
104    /// Exit code to use when exiting
105    pub exit_code: i32,
106    /// Flag to indicate pending signals need processing
107    /// Set by signal handler, checked by executor
108    #[allow(dead_code)]
109    pub pending_signals: bool,
110    /// Pending here-document content from script execution
111    pub pending_heredoc_content: Option<String>,
112    /// Interactive mode heredoc collection state
113    pub collecting_heredoc: Option<(String, String, String)>, // (command_line, delimiter, collected_content)
114    /// File descriptor table for managing open file descriptors
115    pub fd_table: Rc<RefCell<FileDescriptorTable>>,
116    /// Current subshell nesting depth (for recursion limit)
117    pub subshell_depth: usize,
118    /// Override for stdin (used for pipeline subshells to avoid process-global fd manipulation)
119    pub stdin_override: Option<RawFd>,
120    /// Shell option flags (set builtin)
121    pub options: ShellOptions,
122    /// Context tracking for errexit option - true when executing commands in if/while/until conditions
123    pub in_condition: bool,
124    /// Context tracking for errexit option - true when executing commands in && or || chains
125    pub in_logical_chain: bool,
126    /// Context tracking for errexit option - true when executing negated commands (!)
127    pub in_negation: bool,
128    /// Track if the last command executed was a negation (to skip errexit check on inverted code)
129    pub last_was_negation: bool,
130    /// Current line number in script execution (for $LINENO)
131    pub current_line_number: usize,
132    /// Stack of line numbers for function calls (to restore after function returns)
133    pub line_number_stack: Vec<usize>,
134}
135
136impl ShellState {
137    /// Creates a new ShellState initialized with sensible defaults and environment-derived settings.
138    ///
139    /// The returned state initializes runtime fields (variables, exported, aliases, positional params, function/local scopes, FD table, traps, and control flags) and derives display preferences from environment:
140    /// - `colors_enabled` is determined by `NO_COLOR`, `RUSH_COLORS`, and whether stdout is a terminal.
141    /// - `condensed_cwd` is determined by `RUSH_CONDENSED` (defaults to `true`).
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use rush_sh::ShellState;
147    /// let state = ShellState::new();
148    /// // basic invariants
149    /// assert_eq!(state.last_exit_code, 0);
150    /// assert!(state.shell_pid != 0);
151    /// ```
152    pub fn new() -> Self {
153        let shell_pid = std::process::id();
154
155        // Check NO_COLOR environment variable (respects standard)
156        let no_color = env::var("NO_COLOR").is_ok();
157
158        // Check RUSH_COLORS environment variable for explicit control
159        let rush_colors = env::var("RUSH_COLORS")
160            .map(|v| v.to_lowercase())
161            .unwrap_or_else(|_| "auto".to_string());
162
163        let colors_enabled = match rush_colors.as_str() {
164            "1" | "true" | "on" | "enable" => !no_color && std::io::stdout().is_terminal(),
165            "0" | "false" | "off" | "disable" => false,
166            "auto" => !no_color && std::io::stdout().is_terminal(),
167            _ => !no_color && std::io::stdout().is_terminal(),
168        };
169
170        // Check RUSH_CONDENSED environment variable for cwd display preference
171        let rush_condensed = env::var("RUSH_CONDENSED")
172            .map(|v| v.to_lowercase())
173            .unwrap_or_else(|_| "true".to_string());
174
175        let condensed_cwd = match rush_condensed.as_str() {
176            "1" | "true" | "on" | "enable" => true,
177            "0" | "false" | "off" | "disable" => false,
178            _ => true, // Default to condensed for backward compatibility
179        };
180
181        Self {
182            variables: HashMap::new(),
183            exported: HashSet::new(),
184            last_exit_code: 0,
185            shell_pid,
186            script_name: "rush".to_string(),
187            dir_stack: Vec::new(),
188            aliases: HashMap::new(),
189            colors_enabled,
190            color_scheme: ColorScheme::default(),
191            positional_params: Vec::new(),
192            functions: HashMap::new(),
193            local_vars: Vec::new(),
194            function_depth: 0,
195            max_recursion_depth: 500, // Default recursion limit (reduced to avoid Rust stack overflow)
196            returning: false,
197            return_value: None,
198            loop_depth: 0,
199            breaking: false,
200            break_level: 0,
201            continuing: false,
202            continue_level: 0,
203            capture_output: None,
204            condensed_cwd,
205            trap_handlers: Arc::new(Mutex::new(HashMap::new())),
206            exit_trap_executed: false,
207            exit_requested: false,
208            exit_code: 0,
209            pending_signals: false,
210            pending_heredoc_content: None,
211            collecting_heredoc: None,
212            fd_table: Rc::new(RefCell::new(FileDescriptorTable::new())),
213            subshell_depth: 0,
214            stdin_override: None,
215            options: ShellOptions::default(),
216            in_condition: false,
217            in_logical_chain: false,
218            in_negation: false,
219            last_was_negation: false,
220            current_line_number: 1,
221            line_number_stack: Vec::new(),
222        }
223    }
224
225    /// Get a variable value, checking local scopes first, then shell variables, then environment
226    pub fn get_var(&self, name: &str) -> Option<String> {
227        // Handle special variables (these are never local)
228        match name {
229            "?" => Some(self.last_exit_code.to_string()),
230            "$" => Some(self.shell_pid.to_string()),
231            "0" => Some(self.script_name.clone()),
232            "LINENO" => Some(self.current_line_number.to_string()),
233            "*" => {
234                // $* - all positional parameters as single string (space-separated)
235                if self.positional_params.is_empty() {
236                    Some("".to_string())
237                } else {
238                    Some(self.positional_params.join(" "))
239                }
240            }
241            "@" => {
242                // $@ - all positional parameters as separate words (but returns as single string for compatibility)
243                if self.positional_params.is_empty() {
244                    Some("".to_string())
245                } else {
246                    Some(self.positional_params.join(" "))
247                }
248            }
249            "#" => Some(self.positional_params.len().to_string()),
250            _ => {
251                // Handle positional parameters $1, $2, $3, etc. (these are never local)
252                if let Ok(index) = name.parse::<usize>()
253                    && index > 0
254                    && index <= self.positional_params.len()
255                {
256                    return Some(self.positional_params[index - 1].clone());
257                }
258
259                // Check local scopes first, then shell variables, then environment
260                // Search local scopes from innermost to outermost
261                for scope in self.local_vars.iter().rev() {
262                    if let Some(value) = scope.get(name) {
263                        return Some(value.clone());
264                    }
265                }
266
267                // Check shell variables
268                if let Some(value) = self.variables.get(name) {
269                    Some(value.clone())
270                } else {
271                    // Fall back to environment variables
272                    env::var(name).ok()
273                }
274            }
275        }
276    }
277
278    /// Set a shell variable (updates local scope if variable exists there, otherwise sets globally)
279    pub fn set_var(&mut self, name: &str, value: String) {
280        // Check if this variable exists in any local scope
281        // If it does, update it there instead of setting globally
282        for scope in self.local_vars.iter_mut().rev() {
283            if scope.contains_key(name) {
284                scope.insert(name.to_string(), value);
285                return;
286            }
287        }
288
289        // Variable doesn't exist in local scopes, set it globally
290        self.variables.insert(name.to_string(), value);
291    }
292
293    /// Remove a shell variable
294    pub fn unset_var(&mut self, name: &str) {
295        self.variables.remove(name);
296        self.exported.remove(name);
297    }
298
299    /// Mark a variable as exported
300    pub fn export_var(&mut self, name: &str) {
301        if self.variables.contains_key(name) {
302            self.exported.insert(name.to_string());
303        }
304    }
305
306    /// Set and export a variable
307    pub fn set_exported_var(&mut self, name: &str, value: String) {
308        self.set_var(name, value);
309        self.export_var(name);
310    }
311
312    /// Get all environment variables for child processes (exported + inherited)
313    pub fn get_env_for_child(&self) -> HashMap<String, String> {
314        let mut child_env = HashMap::new();
315
316        // Add all current environment variables
317        for (key, value) in env::vars() {
318            child_env.insert(key, value);
319        }
320
321        // Override with exported shell variables
322        for var_name in &self.exported {
323            if let Some(value) = self.variables.get(var_name) {
324                child_env.insert(var_name.clone(), value.clone());
325            }
326        }
327
328        child_env
329    }
330
331    /// Update the last exit code
332    pub fn set_last_exit_code(&mut self, code: i32) {
333        self.last_exit_code = code;
334    }
335
336    /// Set the script name ($0)
337    pub fn set_script_name(&mut self, name: &str) {
338        self.script_name = name.to_string();
339    }
340
341    /// Get the condensed current working directory for the prompt
342    pub fn get_condensed_cwd(&self) -> String {
343        match env::current_dir() {
344            Ok(path) => {
345                let path_str = path.to_string_lossy();
346                let components: Vec<&str> = path_str.split('/').collect();
347                if components.is_empty() || (components.len() == 1 && components[0].is_empty()) {
348                    return "/".to_string();
349                }
350                let mut result = String::new();
351                for (i, comp) in components.iter().enumerate() {
352                    if comp.is_empty() {
353                        continue; // skip leading empty component
354                    }
355                    if i == components.len() - 1 {
356                        result.push('/');
357                        result.push_str(comp);
358                    } else {
359                        result.push('/');
360                        if let Some(first) = comp.chars().next() {
361                            result.push(first);
362                        }
363                    }
364                }
365                if result.is_empty() {
366                    "/".to_string()
367                } else {
368                    result
369                }
370            }
371            Err(_) => "/?".to_string(), // fallback if can't get cwd
372        }
373    }
374
375    /// Get the full current working directory for the prompt
376    pub fn get_full_cwd(&self) -> String {
377        match env::current_dir() {
378            Ok(path) => path.to_string_lossy().to_string(),
379            Err(_) => "/?".to_string(), // fallback if can't get cwd
380        }
381    }
382
383    /// Get the user@hostname string for the prompt
384    pub fn get_user_hostname(&self) -> String {
385        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
386
387        // First try to get hostname from HOSTNAME environment variable
388        if let Ok(hostname) = env::var("HOSTNAME")
389            && !hostname.trim().is_empty()
390        {
391            return format!("{}@{}", user, hostname);
392        }
393
394        // If HOSTNAME is not set or empty, try the hostname command
395        let hostname = match std::process::Command::new("hostname").output() {
396            Ok(output) if output.status.success() => {
397                String::from_utf8_lossy(&output.stdout).trim().to_string()
398            }
399            _ => "hostname".to_string(), // Last resort fallback
400        };
401
402        // Set the HOSTNAME environment variable for future use
403        if hostname != "hostname" {
404            unsafe {
405                env::set_var("HOSTNAME", &hostname);
406            }
407        }
408
409        format!("{}@{}", user, hostname)
410    }
411
412    /// Get the full prompt string
413    pub fn get_prompt(&self) -> String {
414        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
415        let prompt_char = if user == "root" { "#" } else { "$" };
416        let cwd = if self.condensed_cwd {
417            self.get_condensed_cwd()
418        } else {
419            self.get_full_cwd()
420        };
421        format!("{}:{} {} ", self.get_user_hostname(), cwd, prompt_char)
422    }
423
424    /// Set an alias
425    pub fn set_alias(&mut self, name: &str, value: String) {
426        self.aliases.insert(name.to_string(), value);
427    }
428
429    /// Get an alias value
430    pub fn get_alias(&self, name: &str) -> Option<&String> {
431        self.aliases.get(name)
432    }
433
434    /// Remove an alias
435    pub fn remove_alias(&mut self, name: &str) {
436        self.aliases.remove(name);
437    }
438
439    /// Get all aliases
440    pub fn get_all_aliases(&self) -> &HashMap<String, String> {
441        &self.aliases
442    }
443
444    /// Set positional parameters
445    pub fn set_positional_params(&mut self, params: Vec<String>) {
446        self.positional_params = params;
447    }
448
449    /// Get positional parameters
450    #[allow(dead_code)]
451    pub fn get_positional_params(&self) -> &[String] {
452        &self.positional_params
453    }
454
455    /// Shift positional parameters (remove first n parameters)
456    pub fn shift_positional_params(&mut self, count: usize) {
457        if count > 0 {
458            for _ in 0..count {
459                if !self.positional_params.is_empty() {
460                    self.positional_params.remove(0);
461                }
462            }
463        }
464    }
465
466    /// Add a positional parameter at the end
467    #[allow(dead_code)]
468    pub fn push_positional_param(&mut self, param: String) {
469        self.positional_params.push(param);
470    }
471
472    /// Define a function
473    pub fn define_function(&mut self, name: String, body: Ast) {
474        self.functions.insert(name, body);
475    }
476
477    /// Get a function definition
478    pub fn get_function(&self, name: &str) -> Option<&Ast> {
479        self.functions.get(name)
480    }
481
482    /// Remove a function definition
483    #[allow(dead_code)]
484    pub fn remove_function(&mut self, name: &str) {
485        self.functions.remove(name);
486    }
487
488    /// Get all function names
489    #[allow(dead_code)]
490    pub fn get_function_names(&self) -> Vec<&String> {
491        self.functions.keys().collect()
492    }
493
494    /// Push a new local variable scope
495    pub fn push_local_scope(&mut self) {
496        self.local_vars.push(HashMap::new());
497    }
498
499    /// Pop the current local variable scope
500    pub fn pop_local_scope(&mut self) {
501        if !self.local_vars.is_empty() {
502            self.local_vars.pop();
503        }
504    }
505
506    /// Set a local variable in the current scope
507    pub fn set_local_var(&mut self, name: &str, value: String) {
508        if let Some(current_scope) = self.local_vars.last_mut() {
509            current_scope.insert(name.to_string(), value);
510        } else {
511            // If no local scope exists, set as global variable
512            self.set_var(name, value);
513        }
514    }
515
516    /// Enter a function context (push local scope if needed)
517    pub fn enter_function(&mut self) {
518        self.function_depth += 1;
519        if self.function_depth > self.local_vars.len() {
520            self.push_local_scope();
521        }
522    }
523
524    /// Exit a function context (pop local scope if needed)
525    pub fn exit_function(&mut self) {
526        if self.function_depth > 0 {
527            self.function_depth -= 1;
528            if self.function_depth == self.local_vars.len() - 1 {
529                self.pop_local_scope();
530            }
531        }
532    }
533
534    /// Set return state for function returns
535    pub fn set_return(&mut self, value: i32) {
536        self.returning = true;
537        self.return_value = Some(value);
538    }
539
540    /// Clear return state
541    pub fn clear_return(&mut self) {
542        self.returning = false;
543        self.return_value = None;
544    }
545
546    /// Check if currently returning
547    pub fn is_returning(&self) -> bool {
548        self.returning
549    }
550
551    /// Get return value if returning
552    pub fn get_return_value(&self) -> Option<i32> {
553        self.return_value
554    }
555
556    /// Enter a loop context (increment loop depth)
557    pub fn enter_loop(&mut self) {
558        self.loop_depth += 1;
559    }
560
561    /// Exit a loop context (decrement loop depth)
562    pub fn exit_loop(&mut self) {
563        if self.loop_depth > 0 {
564            self.loop_depth -= 1;
565        }
566    }
567
568    /// Set break state for loop control
569    pub fn set_break(&mut self, level: usize) {
570        self.breaking = true;
571        self.break_level = level;
572    }
573
574    /// Clear break state
575    pub fn clear_break(&mut self) {
576        self.breaking = false;
577        self.break_level = 0;
578    }
579
580    /// Check if currently breaking
581    pub fn is_breaking(&self) -> bool {
582        self.breaking
583    }
584
585    /// Get break level
586    pub fn get_break_level(&self) -> usize {
587        self.break_level
588    }
589
590    /// Decrement break level (when exiting a loop level)
591    pub fn decrement_break_level(&mut self) {
592        if self.break_level > 0 {
593            self.break_level -= 1;
594        }
595        if self.break_level == 0 {
596            self.breaking = false;
597        }
598    }
599
600    /// Set continue state for loop control
601    pub fn set_continue(&mut self, level: usize) {
602        self.continuing = true;
603        self.continue_level = level;
604    }
605
606    /// Clear continue state
607    pub fn clear_continue(&mut self) {
608        self.continuing = false;
609        self.continue_level = 0;
610    }
611
612    /// Check if currently continuing
613    pub fn is_continuing(&self) -> bool {
614        self.continuing
615    }
616
617    /// Get continue level
618    pub fn get_continue_level(&self) -> usize {
619        self.continue_level
620    }
621
622    /// Decrement continue level (when exiting a loop level)
623    pub fn decrement_continue_level(&mut self) {
624        if self.continue_level > 0 {
625            self.continue_level -= 1;
626        }
627        if self.continue_level == 0 {
628            self.continuing = false;
629        }
630    }
631
632    /// Set a trap handler for a signal
633    pub fn set_trap(&mut self, signal: &str, command: String) {
634        if let Ok(mut handlers) = self.trap_handlers.lock() {
635            handlers.insert(signal.to_uppercase(), command);
636        }
637    }
638
639    /// Get a trap handler for a signal
640    pub fn get_trap(&self, signal: &str) -> Option<String> {
641        if let Ok(handlers) = self.trap_handlers.lock() {
642            handlers.get(&signal.to_uppercase()).cloned()
643        } else {
644            None
645        }
646    }
647
648    /// Remove a trap handler for a signal
649    pub fn remove_trap(&mut self, signal: &str) {
650        if let Ok(mut handlers) = self.trap_handlers.lock() {
651            handlers.remove(&signal.to_uppercase());
652        }
653    }
654
655    /// Get all trap handlers
656    pub fn get_all_traps(&self) -> HashMap<String, String> {
657        if let Ok(handlers) = self.trap_handlers.lock() {
658            handlers.clone()
659        } else {
660            HashMap::new()
661        }
662    }
663
664    /// Clear all trap handlers
665    #[allow(dead_code)]
666    pub fn clear_traps(&mut self) {
667        if let Ok(mut handlers) = self.trap_handlers.lock() {
668            handlers.clear();
669        }
670    }
671}
672
673impl Default for ShellState {
674    fn default() -> Self {
675        Self::new()
676    }
677}
678
679#[cfg(test)]
680mod tests;