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