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}
131
132impl ShellState {
133    /// Creates a new ShellState initialized with sensible defaults and environment-derived settings.
134    ///
135    /// 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:
136    /// - `colors_enabled` is determined by `NO_COLOR`, `RUSH_COLORS`, and whether stdout is a terminal.
137    /// - `condensed_cwd` is determined by `RUSH_CONDENSED` (defaults to `true`).
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// use rush_sh::ShellState;
143    /// let state = ShellState::new();
144    /// // basic invariants
145    /// assert_eq!(state.last_exit_code, 0);
146    /// assert!(state.shell_pid != 0);
147    /// ```
148    pub fn new() -> Self {
149        let shell_pid = std::process::id();
150
151        // Check NO_COLOR environment variable (respects standard)
152        let no_color = env::var("NO_COLOR").is_ok();
153
154        // Check RUSH_COLORS environment variable for explicit control
155        let rush_colors = env::var("RUSH_COLORS")
156            .map(|v| v.to_lowercase())
157            .unwrap_or_else(|_| "auto".to_string());
158
159        let colors_enabled = match rush_colors.as_str() {
160            "1" | "true" | "on" | "enable" => !no_color && std::io::stdout().is_terminal(),
161            "0" | "false" | "off" | "disable" => false,
162            "auto" => !no_color && std::io::stdout().is_terminal(),
163            _ => !no_color && std::io::stdout().is_terminal(),
164        };
165
166        // Check RUSH_CONDENSED environment variable for cwd display preference
167        let rush_condensed = env::var("RUSH_CONDENSED")
168            .map(|v| v.to_lowercase())
169            .unwrap_or_else(|_| "true".to_string());
170
171        let condensed_cwd = match rush_condensed.as_str() {
172            "1" | "true" | "on" | "enable" => true,
173            "0" | "false" | "off" | "disable" => false,
174            _ => true, // Default to condensed for backward compatibility
175        };
176
177        Self {
178            variables: HashMap::new(),
179            exported: HashSet::new(),
180            last_exit_code: 0,
181            shell_pid,
182            script_name: "rush".to_string(),
183            dir_stack: Vec::new(),
184            aliases: HashMap::new(),
185            colors_enabled,
186            color_scheme: ColorScheme::default(),
187            positional_params: Vec::new(),
188            functions: HashMap::new(),
189            local_vars: Vec::new(),
190            function_depth: 0,
191            max_recursion_depth: 500, // Default recursion limit (reduced to avoid Rust stack overflow)
192            returning: false,
193            return_value: None,
194            loop_depth: 0,
195            breaking: false,
196            break_level: 0,
197            continuing: false,
198            continue_level: 0,
199            capture_output: None,
200            condensed_cwd,
201            trap_handlers: Arc::new(Mutex::new(HashMap::new())),
202            exit_trap_executed: false,
203            exit_requested: false,
204            exit_code: 0,
205            pending_signals: false,
206            pending_heredoc_content: None,
207            collecting_heredoc: None,
208            fd_table: Rc::new(RefCell::new(FileDescriptorTable::new())),
209            subshell_depth: 0,
210            stdin_override: None,
211            options: ShellOptions::default(),
212            in_condition: false,
213            in_logical_chain: false,
214            in_negation: false,
215            last_was_negation: false,
216        }
217    }
218
219    /// Get a variable value, checking local scopes first, then shell variables, then environment
220    pub fn get_var(&self, name: &str) -> Option<String> {
221        // Handle special variables (these are never local)
222        match name {
223            "?" => Some(self.last_exit_code.to_string()),
224            "$" => Some(self.shell_pid.to_string()),
225            "0" => Some(self.script_name.clone()),
226            "*" => {
227                // $* - all positional parameters as single string (space-separated)
228                if self.positional_params.is_empty() {
229                    Some("".to_string())
230                } else {
231                    Some(self.positional_params.join(" "))
232                }
233            }
234            "@" => {
235                // $@ - all positional parameters as separate words (but returns as single string for compatibility)
236                if self.positional_params.is_empty() {
237                    Some("".to_string())
238                } else {
239                    Some(self.positional_params.join(" "))
240                }
241            }
242            "#" => Some(self.positional_params.len().to_string()),
243            _ => {
244                // Handle positional parameters $1, $2, $3, etc. (these are never local)
245                if let Ok(index) = name.parse::<usize>()
246                    && index > 0
247                    && index <= self.positional_params.len()
248                {
249                    return Some(self.positional_params[index - 1].clone());
250                }
251
252                // Check local scopes first, then shell variables, then environment
253                // Search local scopes from innermost to outermost
254                for scope in self.local_vars.iter().rev() {
255                    if let Some(value) = scope.get(name) {
256                        return Some(value.clone());
257                    }
258                }
259
260                // Check shell variables
261                if let Some(value) = self.variables.get(name) {
262                    Some(value.clone())
263                } else {
264                    // Fall back to environment variables
265                    env::var(name).ok()
266                }
267            }
268        }
269    }
270
271    /// Set a shell variable (updates local scope if variable exists there, otherwise sets globally)
272    pub fn set_var(&mut self, name: &str, value: String) {
273        // Check if this variable exists in any local scope
274        // If it does, update it there instead of setting globally
275        for scope in self.local_vars.iter_mut().rev() {
276            if scope.contains_key(name) {
277                scope.insert(name.to_string(), value);
278                return;
279            }
280        }
281
282        // Variable doesn't exist in local scopes, set it globally
283        self.variables.insert(name.to_string(), value);
284    }
285
286    /// Remove a shell variable
287    pub fn unset_var(&mut self, name: &str) {
288        self.variables.remove(name);
289        self.exported.remove(name);
290    }
291
292    /// Mark a variable as exported
293    pub fn export_var(&mut self, name: &str) {
294        if self.variables.contains_key(name) {
295            self.exported.insert(name.to_string());
296        }
297    }
298
299    /// Set and export a variable
300    pub fn set_exported_var(&mut self, name: &str, value: String) {
301        self.set_var(name, value);
302        self.export_var(name);
303    }
304
305    /// Get all environment variables for child processes (exported + inherited)
306    pub fn get_env_for_child(&self) -> HashMap<String, String> {
307        let mut child_env = HashMap::new();
308
309        // Add all current environment variables
310        for (key, value) in env::vars() {
311            child_env.insert(key, value);
312        }
313
314        // Override with exported shell variables
315        for var_name in &self.exported {
316            if let Some(value) = self.variables.get(var_name) {
317                child_env.insert(var_name.clone(), value.clone());
318            }
319        }
320
321        child_env
322    }
323
324    /// Update the last exit code
325    pub fn set_last_exit_code(&mut self, code: i32) {
326        self.last_exit_code = code;
327    }
328
329    /// Set the script name ($0)
330    pub fn set_script_name(&mut self, name: &str) {
331        self.script_name = name.to_string();
332    }
333
334    /// Get the condensed current working directory for the prompt
335    pub fn get_condensed_cwd(&self) -> String {
336        match env::current_dir() {
337            Ok(path) => {
338                let path_str = path.to_string_lossy();
339                let components: Vec<&str> = path_str.split('/').collect();
340                if components.is_empty() || (components.len() == 1 && components[0].is_empty()) {
341                    return "/".to_string();
342                }
343                let mut result = String::new();
344                for (i, comp) in components.iter().enumerate() {
345                    if comp.is_empty() {
346                        continue; // skip leading empty component
347                    }
348                    if i == components.len() - 1 {
349                        result.push('/');
350                        result.push_str(comp);
351                    } else {
352                        result.push('/');
353                        if let Some(first) = comp.chars().next() {
354                            result.push(first);
355                        }
356                    }
357                }
358                if result.is_empty() {
359                    "/".to_string()
360                } else {
361                    result
362                }
363            }
364            Err(_) => "/?".to_string(), // fallback if can't get cwd
365        }
366    }
367
368    /// Get the full current working directory for the prompt
369    pub fn get_full_cwd(&self) -> String {
370        match env::current_dir() {
371            Ok(path) => path.to_string_lossy().to_string(),
372            Err(_) => "/?".to_string(), // fallback if can't get cwd
373        }
374    }
375
376    /// Get the user@hostname string for the prompt
377    pub fn get_user_hostname(&self) -> String {
378        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
379
380        // First try to get hostname from HOSTNAME environment variable
381        if let Ok(hostname) = env::var("HOSTNAME")
382            && !hostname.trim().is_empty()
383        {
384            return format!("{}@{}", user, hostname);
385        }
386
387        // If HOSTNAME is not set or empty, try the hostname command
388        let hostname = match std::process::Command::new("hostname").output() {
389            Ok(output) if output.status.success() => {
390                String::from_utf8_lossy(&output.stdout).trim().to_string()
391            }
392            _ => "hostname".to_string(), // Last resort fallback
393        };
394
395        // Set the HOSTNAME environment variable for future use
396        if hostname != "hostname" {
397            unsafe {
398                env::set_var("HOSTNAME", &hostname);
399            }
400        }
401
402        format!("{}@{}", user, hostname)
403    }
404
405    /// Get the full prompt string
406    pub fn get_prompt(&self) -> String {
407        let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
408        let prompt_char = if user == "root" { "#" } else { "$" };
409        let cwd = if self.condensed_cwd {
410            self.get_condensed_cwd()
411        } else {
412            self.get_full_cwd()
413        };
414        format!("{}:{} {} ", self.get_user_hostname(), cwd, prompt_char)
415    }
416
417    /// Set an alias
418    pub fn set_alias(&mut self, name: &str, value: String) {
419        self.aliases.insert(name.to_string(), value);
420    }
421
422    /// Get an alias value
423    pub fn get_alias(&self, name: &str) -> Option<&String> {
424        self.aliases.get(name)
425    }
426
427    /// Remove an alias
428    pub fn remove_alias(&mut self, name: &str) {
429        self.aliases.remove(name);
430    }
431
432    /// Get all aliases
433    pub fn get_all_aliases(&self) -> &HashMap<String, String> {
434        &self.aliases
435    }
436
437    /// Set positional parameters
438    pub fn set_positional_params(&mut self, params: Vec<String>) {
439        self.positional_params = params;
440    }
441
442    /// Get positional parameters
443    #[allow(dead_code)]
444    pub fn get_positional_params(&self) -> &[String] {
445        &self.positional_params
446    }
447
448    /// Shift positional parameters (remove first n parameters)
449    pub fn shift_positional_params(&mut self, count: usize) {
450        if count > 0 {
451            for _ in 0..count {
452                if !self.positional_params.is_empty() {
453                    self.positional_params.remove(0);
454                }
455            }
456        }
457    }
458
459    /// Add a positional parameter at the end
460    #[allow(dead_code)]
461    pub fn push_positional_param(&mut self, param: String) {
462        self.positional_params.push(param);
463    }
464
465    /// Define a function
466    pub fn define_function(&mut self, name: String, body: Ast) {
467        self.functions.insert(name, body);
468    }
469
470    /// Get a function definition
471    pub fn get_function(&self, name: &str) -> Option<&Ast> {
472        self.functions.get(name)
473    }
474
475    /// Remove a function definition
476    #[allow(dead_code)]
477    pub fn remove_function(&mut self, name: &str) {
478        self.functions.remove(name);
479    }
480
481    /// Get all function names
482    #[allow(dead_code)]
483    pub fn get_function_names(&self) -> Vec<&String> {
484        self.functions.keys().collect()
485    }
486
487    /// Push a new local variable scope
488    pub fn push_local_scope(&mut self) {
489        self.local_vars.push(HashMap::new());
490    }
491
492    /// Pop the current local variable scope
493    pub fn pop_local_scope(&mut self) {
494        if !self.local_vars.is_empty() {
495            self.local_vars.pop();
496        }
497    }
498
499    /// Set a local variable in the current scope
500    pub fn set_local_var(&mut self, name: &str, value: String) {
501        if let Some(current_scope) = self.local_vars.last_mut() {
502            current_scope.insert(name.to_string(), value);
503        } else {
504            // If no local scope exists, set as global variable
505            self.set_var(name, value);
506        }
507    }
508
509    /// Enter a function context (push local scope if needed)
510    pub fn enter_function(&mut self) {
511        self.function_depth += 1;
512        if self.function_depth > self.local_vars.len() {
513            self.push_local_scope();
514        }
515    }
516
517    /// Exit a function context (pop local scope if needed)
518    pub fn exit_function(&mut self) {
519        if self.function_depth > 0 {
520            self.function_depth -= 1;
521            if self.function_depth == self.local_vars.len() - 1 {
522                self.pop_local_scope();
523            }
524        }
525    }
526
527    /// Set return state for function returns
528    pub fn set_return(&mut self, value: i32) {
529        self.returning = true;
530        self.return_value = Some(value);
531    }
532
533    /// Clear return state
534    pub fn clear_return(&mut self) {
535        self.returning = false;
536        self.return_value = None;
537    }
538
539    /// Check if currently returning
540    pub fn is_returning(&self) -> bool {
541        self.returning
542    }
543
544    /// Get return value if returning
545    pub fn get_return_value(&self) -> Option<i32> {
546        self.return_value
547    }
548
549    /// Enter a loop context (increment loop depth)
550    pub fn enter_loop(&mut self) {
551        self.loop_depth += 1;
552    }
553
554    /// Exit a loop context (decrement loop depth)
555    pub fn exit_loop(&mut self) {
556        if self.loop_depth > 0 {
557            self.loop_depth -= 1;
558        }
559    }
560
561    /// Set break state for loop control
562    pub fn set_break(&mut self, level: usize) {
563        self.breaking = true;
564        self.break_level = level;
565    }
566
567    /// Clear break state
568    pub fn clear_break(&mut self) {
569        self.breaking = false;
570        self.break_level = 0;
571    }
572
573    /// Check if currently breaking
574    pub fn is_breaking(&self) -> bool {
575        self.breaking
576    }
577
578    /// Get break level
579    pub fn get_break_level(&self) -> usize {
580        self.break_level
581    }
582
583    /// Decrement break level (when exiting a loop level)
584    pub fn decrement_break_level(&mut self) {
585        if self.break_level > 0 {
586            self.break_level -= 1;
587        }
588        if self.break_level == 0 {
589            self.breaking = false;
590        }
591    }
592
593    /// Set continue state for loop control
594    pub fn set_continue(&mut self, level: usize) {
595        self.continuing = true;
596        self.continue_level = level;
597    }
598
599    /// Clear continue state
600    pub fn clear_continue(&mut self) {
601        self.continuing = false;
602        self.continue_level = 0;
603    }
604
605    /// Check if currently continuing
606    pub fn is_continuing(&self) -> bool {
607        self.continuing
608    }
609
610    /// Get continue level
611    pub fn get_continue_level(&self) -> usize {
612        self.continue_level
613    }
614
615    /// Decrement continue level (when exiting a loop level)
616    pub fn decrement_continue_level(&mut self) {
617        if self.continue_level > 0 {
618            self.continue_level -= 1;
619        }
620        if self.continue_level == 0 {
621            self.continuing = false;
622        }
623    }
624
625    /// Set a trap handler for a signal
626    pub fn set_trap(&mut self, signal: &str, command: String) {
627        if let Ok(mut handlers) = self.trap_handlers.lock() {
628            handlers.insert(signal.to_uppercase(), command);
629        }
630    }
631
632    /// Get a trap handler for a signal
633    pub fn get_trap(&self, signal: &str) -> Option<String> {
634        if let Ok(handlers) = self.trap_handlers.lock() {
635            handlers.get(&signal.to_uppercase()).cloned()
636        } else {
637            None
638        }
639    }
640
641    /// Remove a trap handler for a signal
642    pub fn remove_trap(&mut self, signal: &str) {
643        if let Ok(mut handlers) = self.trap_handlers.lock() {
644            handlers.remove(&signal.to_uppercase());
645        }
646    }
647
648    /// Get all trap handlers
649    pub fn get_all_traps(&self) -> HashMap<String, String> {
650        if let Ok(handlers) = self.trap_handlers.lock() {
651            handlers.clone()
652        } else {
653            HashMap::new()
654        }
655    }
656
657    /// Clear all trap handlers
658    #[allow(dead_code)]
659    pub fn clear_traps(&mut self) {
660        if let Ok(mut handlers) = self.trap_handlers.lock() {
661            handlers.clear();
662        }
663    }
664}
665
666impl Default for ShellState {
667    fn default() -> Self {
668        Self::new()
669    }
670}
671
672#[cfg(test)]
673mod tests;