1use std::collections::{HashMap, HashSet};
2use std::rc::Rc;
3use std::cell::RefCell;
4use super::parser::Ast;
5use std::env;
6use std::io::IsTerminal;
7
8#[derive(Debug, Clone)]
9pub struct ColorScheme {
10 pub prompt: String,
12 pub error: String,
14 pub success: String,
16 pub builtin: String,
18 pub directory: String,
20}
21
22impl Default for ColorScheme {
23 fn default() -> Self {
24 Self {
25 prompt: "\x1b[32m".to_string(), error: "\x1b[31m".to_string(), success: "\x1b[32m".to_string(), builtin: "\x1b[36m".to_string(), directory: "\x1b[34m".to_string(), }
31 }
32}
33
34#[derive(Debug, Clone)]
35pub struct ShellState {
36 pub variables: HashMap<String, String>,
38 pub exported: HashSet<String>,
40 pub last_exit_code: i32,
42 pub shell_pid: u32,
44 pub script_name: String,
46 pub dir_stack: Vec<String>,
48 pub aliases: HashMap<String, String>,
50 pub colors_enabled: bool,
52 pub color_scheme: ColorScheme,
54 pub positional_params: Vec<String>,
56 pub functions: HashMap<String, Ast>,
58 pub local_vars: Vec<HashMap<String, String>>,
60 pub function_depth: usize,
62 pub max_recursion_depth: usize,
64 pub returning: bool,
66 pub return_value: Option<i32>,
68 pub capture_output: Option<Rc<RefCell<Vec<u8>>>>,
70 pub condensed_cwd: bool,
72}
73
74impl ShellState {
75 pub fn new() -> Self {
76 let shell_pid = std::process::id();
77
78 let no_color = std::env::var("NO_COLOR").is_ok();
80
81 let rush_colors = std::env::var("RUSH_COLORS")
83 .map(|v| v.to_lowercase())
84 .unwrap_or_else(|_| "auto".to_string());
85
86 let colors_enabled = match rush_colors.as_str() {
87 "1" | "true" | "on" | "enable" => !no_color && std::io::stdout().is_terminal(),
88 "0" | "false" | "off" | "disable" => false,
89 "auto" => !no_color && std::io::stdout().is_terminal(),
90 _ => !no_color && std::io::stdout().is_terminal(),
91 };
92
93 let rush_condensed = std::env::var("RUSH_CONDENSED")
95 .map(|v| v.to_lowercase())
96 .unwrap_or_else(|_| "true".to_string());
97
98 let condensed_cwd = match rush_condensed.as_str() {
99 "1" | "true" | "on" | "enable" => true,
100 "0" | "false" | "off" | "disable" => false,
101 _ => true, };
103
104 Self {
105 variables: HashMap::new(),
106 exported: HashSet::new(),
107 last_exit_code: 0,
108 shell_pid,
109 script_name: "rush".to_string(),
110 dir_stack: Vec::new(),
111 aliases: HashMap::new(),
112 colors_enabled,
113 color_scheme: ColorScheme::default(),
114 positional_params: Vec::new(),
115 functions: HashMap::new(),
116 local_vars: Vec::new(),
117 function_depth: 0,
118 max_recursion_depth: 500, returning: false,
120 return_value: None,
121 capture_output: None,
122 condensed_cwd,
123 }
124 }
125
126 pub fn get_var(&self, name: &str) -> Option<String> {
128 match name {
130 "?" => Some(self.last_exit_code.to_string()),
131 "$" => Some(self.shell_pid.to_string()),
132 "0" => Some(self.script_name.clone()),
133 "*" => {
134 if self.positional_params.is_empty() {
136 Some("".to_string())
137 } else {
138 Some(self.positional_params.join(" "))
139 }
140 }
141 "@" => {
142 if self.positional_params.is_empty() {
144 Some("".to_string())
145 } else {
146 Some(self.positional_params.join(" "))
147 }
148 }
149 "#" => Some(self.positional_params.len().to_string()),
150 _ => {
151 if let Ok(index) = name.parse::<usize>()
153 && index > 0
154 && index <= self.positional_params.len()
155 {
156 return Some(self.positional_params[index - 1].clone());
157 }
158
159 for scope in self.local_vars.iter().rev() {
162 if let Some(value) = scope.get(name) {
163 return Some(value.clone());
164 }
165 }
166
167 if let Some(value) = self.variables.get(name) {
169 Some(value.clone())
170 } else {
171 env::var(name).ok()
173 }
174 }
175 }
176 }
177
178 pub fn set_var(&mut self, name: &str, value: String) {
180 for scope in self.local_vars.iter_mut().rev() {
183 if scope.contains_key(name) {
184 scope.insert(name.to_string(), value);
185 return;
186 }
187 }
188
189 self.variables.insert(name.to_string(), value);
191 }
192
193 pub fn unset_var(&mut self, name: &str) {
195 self.variables.remove(name);
196 self.exported.remove(name);
197 }
198
199 pub fn export_var(&mut self, name: &str) {
201 if self.variables.contains_key(name) {
202 self.exported.insert(name.to_string());
203 }
204 }
205
206 pub fn set_exported_var(&mut self, name: &str, value: String) {
208 self.set_var(name, value);
209 self.export_var(name);
210 }
211
212 pub fn get_env_for_child(&self) -> HashMap<String, String> {
214 let mut child_env = HashMap::new();
215
216 for (key, value) in env::vars() {
218 child_env.insert(key, value);
219 }
220
221 for var_name in &self.exported {
223 if let Some(value) = self.variables.get(var_name) {
224 child_env.insert(var_name.clone(), value.clone());
225 }
226 }
227
228 child_env
229 }
230
231 pub fn set_last_exit_code(&mut self, code: i32) {
233 self.last_exit_code = code;
234 }
235
236 pub fn set_script_name(&mut self, name: &str) {
238 self.script_name = name.to_string();
239 }
240
241 pub fn get_condensed_cwd(&self) -> String {
243 match std::env::current_dir() {
244 Ok(path) => {
245 let path_str = path.to_string_lossy();
246 let components: Vec<&str> = path_str.split('/').collect();
247 if components.is_empty() || (components.len() == 1 && components[0].is_empty()) {
248 return "/".to_string();
249 }
250 let mut result = String::new();
251 for (i, comp) in components.iter().enumerate() {
252 if comp.is_empty() {
253 continue; }
255 if i == components.len() - 1 {
256 result.push('/');
257 result.push_str(comp);
258 } else {
259 result.push('/');
260 if let Some(first) = comp.chars().next() {
261 result.push(first);
262 }
263 }
264 }
265 if result.is_empty() {
266 "/".to_string()
267 } else {
268 result
269 }
270 }
271 Err(_) => "/?".to_string(), }
273 }
274
275 pub fn get_full_cwd(&self) -> String {
277 match std::env::current_dir() {
278 Ok(path) => path.to_string_lossy().to_string(),
279 Err(_) => "/?".to_string(), }
281 }
282
283 pub fn get_user_hostname(&self) -> String {
285 let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
286 let hostname = env::var("HOSTNAME").unwrap_or_else(|_| "hostname".to_string());
287 format!("{}@{}", user, hostname)
288 }
289
290 pub fn get_prompt(&self) -> String {
292 let user = env::var("USER").unwrap_or_else(|_| "user".to_string());
293 let prompt_char = if user == "root" { "#" } else { "$" };
294 let cwd = if self.condensed_cwd {
295 self.get_condensed_cwd()
296 } else {
297 self.get_full_cwd()
298 };
299 format!(
300 "{}:{} {} ",
301 self.get_user_hostname(),
302 cwd,
303 prompt_char
304 )
305 }
306
307 pub fn set_alias(&mut self, name: &str, value: String) {
309 self.aliases.insert(name.to_string(), value);
310 }
311
312 pub fn get_alias(&self, name: &str) -> Option<&String> {
314 self.aliases.get(name)
315 }
316
317 pub fn remove_alias(&mut self, name: &str) {
319 self.aliases.remove(name);
320 }
321
322 pub fn get_all_aliases(&self) -> &HashMap<String, String> {
324 &self.aliases
325 }
326
327 pub fn set_positional_params(&mut self, params: Vec<String>) {
329 self.positional_params = params;
330 }
331
332 #[allow(dead_code)]
334 pub fn get_positional_params(&self) -> &[String] {
335 &self.positional_params
336 }
337
338 pub fn shift_positional_params(&mut self, count: usize) {
340 if count > 0 {
341 for _ in 0..count {
342 if !self.positional_params.is_empty() {
343 self.positional_params.remove(0);
344 }
345 }
346 }
347 }
348
349 #[allow(dead_code)]
351 pub fn push_positional_param(&mut self, param: String) {
352 self.positional_params.push(param);
353 }
354
355 pub fn define_function(&mut self, name: String, body: Ast) {
357 self.functions.insert(name, body);
358 }
359
360 pub fn get_function(&self, name: &str) -> Option<&Ast> {
362 self.functions.get(name)
363 }
364
365 #[allow(dead_code)]
367 pub fn remove_function(&mut self, name: &str) {
368 self.functions.remove(name);
369 }
370
371 #[allow(dead_code)]
373 pub fn get_function_names(&self) -> Vec<&String> {
374 self.functions.keys().collect()
375 }
376
377 pub fn push_local_scope(&mut self) {
379 self.local_vars.push(HashMap::new());
380 }
381
382 pub fn pop_local_scope(&mut self) {
384 if !self.local_vars.is_empty() {
385 self.local_vars.pop();
386 }
387 }
388
389 pub fn set_local_var(&mut self, name: &str, value: String) {
391 if let Some(current_scope) = self.local_vars.last_mut() {
392 current_scope.insert(name.to_string(), value);
393 } else {
394 self.set_var(name, value);
396 }
397 }
398
399
400 pub fn enter_function(&mut self) {
402 self.function_depth += 1;
403 if self.function_depth > self.local_vars.len() {
404 self.push_local_scope();
405 }
406 }
407
408 pub fn exit_function(&mut self) {
410 if self.function_depth > 0 {
411 self.function_depth -= 1;
412 if self.function_depth == self.local_vars.len() - 1 {
413 self.pop_local_scope();
414 }
415 }
416 }
417
418 pub fn set_return(&mut self, value: i32) {
420 self.returning = true;
421 self.return_value = Some(value);
422 }
423
424 pub fn clear_return(&mut self) {
426 self.returning = false;
427 self.return_value = None;
428 }
429
430 pub fn is_returning(&self) -> bool {
432 self.returning
433 }
434
435 pub fn get_return_value(&self) -> Option<i32> {
437 self.return_value
438 }
439
440
441}
442
443
444impl Default for ShellState {
445 fn default() -> Self {
446 Self::new()
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn test_shell_state_basic() {
456 let mut state = ShellState::new();
457 state.set_var("TEST_VAR", "test_value".to_string());
458 assert_eq!(state.get_var("TEST_VAR"), Some("test_value".to_string()));
459 }
460
461 #[test]
462 fn test_special_variables() {
463 let mut state = ShellState::new();
464 state.set_last_exit_code(42);
465 state.set_script_name("test_script");
466
467 assert_eq!(state.get_var("?"), Some("42".to_string()));
468 assert_eq!(state.get_var("$"), Some(state.shell_pid.to_string()));
469 assert_eq!(state.get_var("0"), Some("test_script".to_string()));
470 }
471
472 #[test]
473 fn test_export_variable() {
474 let mut state = ShellState::new();
475 state.set_var("EXPORT_VAR", "export_value".to_string());
476 state.export_var("EXPORT_VAR");
477
478 let child_env = state.get_env_for_child();
479 assert_eq!(
480 child_env.get("EXPORT_VAR"),
481 Some(&"export_value".to_string())
482 );
483 }
484
485 #[test]
486 fn test_unset_variable() {
487 let mut state = ShellState::new();
488 state.set_var("UNSET_VAR", "value".to_string());
489 state.export_var("UNSET_VAR");
490
491 assert!(state.variables.contains_key("UNSET_VAR"));
492 assert!(state.exported.contains("UNSET_VAR"));
493
494 state.unset_var("UNSET_VAR");
495
496 assert!(!state.variables.contains_key("UNSET_VAR"));
497 assert!(!state.exported.contains("UNSET_VAR"));
498 }
499
500 #[test]
501 fn test_get_user_hostname() {
502 let state = ShellState::new();
503 let user_hostname = state.get_user_hostname();
504 assert!(user_hostname.contains('@'));
506 }
507
508 #[test]
509 fn test_get_prompt() {
510 let state = ShellState::new();
511 let prompt = state.get_prompt();
512 assert!(prompt.ends_with(" $ "));
514 assert!(prompt.contains('@'));
515 }
516
517 #[test]
518 fn test_positional_parameters() {
519 let mut state = ShellState::new();
520 state.set_positional_params(vec![
521 "arg1".to_string(),
522 "arg2".to_string(),
523 "arg3".to_string(),
524 ]);
525
526 assert_eq!(state.get_var("1"), Some("arg1".to_string()));
527 assert_eq!(state.get_var("2"), Some("arg2".to_string()));
528 assert_eq!(state.get_var("3"), Some("arg3".to_string()));
529 assert_eq!(state.get_var("4"), None);
530 assert_eq!(state.get_var("#"), Some("3".to_string()));
531 assert_eq!(state.get_var("*"), Some("arg1 arg2 arg3".to_string()));
532 assert_eq!(state.get_var("@"), Some("arg1 arg2 arg3".to_string()));
533 }
534
535 #[test]
536 fn test_positional_parameters_empty() {
537 let mut state = ShellState::new();
538 state.set_positional_params(vec![]);
539
540 assert_eq!(state.get_var("1"), None);
541 assert_eq!(state.get_var("#"), Some("0".to_string()));
542 assert_eq!(state.get_var("*"), Some("".to_string()));
543 assert_eq!(state.get_var("@"), Some("".to_string()));
544 }
545
546 #[test]
547 fn test_shift_positional_params() {
548 let mut state = ShellState::new();
549 state.set_positional_params(vec![
550 "arg1".to_string(),
551 "arg2".to_string(),
552 "arg3".to_string(),
553 ]);
554
555 assert_eq!(state.get_var("1"), Some("arg1".to_string()));
556 assert_eq!(state.get_var("2"), Some("arg2".to_string()));
557 assert_eq!(state.get_var("3"), Some("arg3".to_string()));
558
559 state.shift_positional_params(1);
560
561 assert_eq!(state.get_var("1"), Some("arg2".to_string()));
562 assert_eq!(state.get_var("2"), Some("arg3".to_string()));
563 assert_eq!(state.get_var("3"), None);
564 assert_eq!(state.get_var("#"), Some("2".to_string()));
565
566 state.shift_positional_params(2);
567
568 assert_eq!(state.get_var("1"), None);
569 assert_eq!(state.get_var("#"), Some("0".to_string()));
570 }
571
572 #[test]
573 fn test_push_positional_param() {
574 let mut state = ShellState::new();
575 state.set_positional_params(vec!["arg1".to_string()]);
576
577 assert_eq!(state.get_var("1"), Some("arg1".to_string()));
578 assert_eq!(state.get_var("#"), Some("1".to_string()));
579
580 state.push_positional_param("arg2".to_string());
581
582 assert_eq!(state.get_var("1"), Some("arg1".to_string()));
583 assert_eq!(state.get_var("2"), Some("arg2".to_string()));
584 assert_eq!(state.get_var("#"), Some("2".to_string()));
585 }
586
587 #[test]
588 fn test_local_variable_scoping() {
589 let mut state = ShellState::new();
590
591 state.set_var("global_var", "global_value".to_string());
593 assert_eq!(state.get_var("global_var"), Some("global_value".to_string()));
594
595 state.push_local_scope();
597
598 state.set_local_var("global_var", "local_value".to_string());
600 assert_eq!(state.get_var("global_var"), Some("local_value".to_string()));
601
602 state.set_local_var("local_var", "local_only".to_string());
604 assert_eq!(state.get_var("local_var"), Some("local_only".to_string()));
605
606 state.pop_local_scope();
608
609 assert_eq!(state.get_var("global_var"), Some("global_value".to_string()));
611 assert_eq!(state.get_var("local_var"), None);
612 }
613
614 #[test]
615 fn test_nested_local_scopes() {
616 let mut state = ShellState::new();
617
618 state.set_var("test_var", "global".to_string());
620
621 state.push_local_scope();
623 state.set_local_var("test_var", "level1".to_string());
624 assert_eq!(state.get_var("test_var"), Some("level1".to_string()));
625
626 state.push_local_scope();
628 state.set_local_var("test_var", "level2".to_string());
629 assert_eq!(state.get_var("test_var"), Some("level2".to_string()));
630
631 state.pop_local_scope();
633 assert_eq!(state.get_var("test_var"), Some("level1".to_string()));
634
635 state.pop_local_scope();
637 assert_eq!(state.get_var("test_var"), Some("global".to_string()));
638 }
639
640 #[test]
641 fn test_variable_set_in_local_scope() {
642 let mut state = ShellState::new();
643
644 state.set_var("test_var", "global".to_string());
646 assert_eq!(state.get_var("test_var"), Some("global".to_string()));
647
648 state.push_local_scope();
650 state.set_local_var("test_var", "local".to_string());
651 assert_eq!(state.get_var("test_var"), Some("local".to_string()));
652
653 state.pop_local_scope();
655 assert_eq!(state.get_var("test_var"), Some("global".to_string()));
656 }
657
658 #[test]
659 fn test_condensed_cwd_environment_variable() {
660 let state = ShellState::new();
662 assert!(state.condensed_cwd);
663
664 unsafe {
666 std::env::set_var("RUSH_CONDENSED", "true");
667 }
668 let state = ShellState::new();
669 assert!(state.condensed_cwd);
670
671 unsafe {
673 std::env::set_var("RUSH_CONDENSED", "false");
674 }
675 let state = ShellState::new();
676 assert!(!state.condensed_cwd);
677
678 unsafe {
680 std::env::remove_var("RUSH_CONDENSED");
681 }
682 }
683
684 #[test]
685 fn test_get_full_cwd() {
686 let state = ShellState::new();
687 let full_cwd = state.get_full_cwd();
688 assert!(!full_cwd.is_empty());
689 assert!(full_cwd.contains('/') || full_cwd.contains('\\'));
691 }
692
693 #[test]
694 fn test_prompt_with_condensed_setting() {
695 let mut state = ShellState::new();
696
697 assert!(state.condensed_cwd);
699 let prompt_condensed = state.get_prompt();
700 assert!(prompt_condensed.contains('@'));
701
702 state.condensed_cwd = false;
704 let prompt_full = state.get_prompt();
705 assert!(prompt_full.contains('@'));
706
707 assert!(prompt_condensed.ends_with("$ ") || prompt_condensed.ends_with("# "));
709 assert!(prompt_full.ends_with("$ ") || prompt_full.ends_with("# "));
710 }
711}