1use crate::commands::{self, VirtualCommand};
4use crate::error::RustBashError;
5use crate::interpreter::{
6 self, ExecResult, ExecutionCounters, ExecutionLimits, InterpreterState, ShellOpts, ShoptOpts,
7 Variable, VariableAttrs, VariableValue,
8};
9use crate::network::NetworkPolicy;
10use crate::platform::Instant;
11use crate::vfs::{InMemoryFs, VirtualFs};
12use std::collections::HashMap;
13use std::path::Path;
14use std::sync::Arc;
15
16pub struct RustBash {
18 pub(crate) state: InterpreterState,
19}
20
21impl RustBash {
22 pub fn exec(&mut self, input: &str) -> Result<ExecResult, RustBashError> {
24 self.state.counters.reset();
25 self.state.should_exit = false;
26
27 let program = match interpreter::parse(input) {
28 Ok(p) => p,
29 Err(e) => {
30 self.state.last_exit_code = 2;
31 return Err(e);
32 }
33 };
34 let mut result = interpreter::execute_program(&program, &mut self.state)?;
35
36 if let Some(exit_cmd) = self.state.traps.get("EXIT").cloned()
38 && !exit_cmd.is_empty()
39 && !self.state.in_trap
40 {
41 let trap_result = interpreter::execute_trap(&exit_cmd, &mut self.state)?;
42 result.stdout.push_str(&trap_result.stdout);
43 result.stderr.push_str(&trap_result.stderr);
44 }
45
46 Ok(result)
47 }
48
49 pub fn cwd(&self) -> &str {
51 &self.state.cwd
52 }
53
54 pub fn last_exit_code(&self) -> i32 {
56 self.state.last_exit_code
57 }
58
59 pub fn should_exit(&self) -> bool {
61 self.state.should_exit
62 }
63
64 pub fn command_names(&self) -> Vec<&str> {
66 self.state.commands.keys().map(|k| k.as_str()).collect()
67 }
68
69 pub fn command_meta(&self, name: &str) -> Option<&'static commands::CommandMeta> {
71 self.state.commands.get(name).and_then(|cmd| cmd.meta())
72 }
73
74 pub fn set_shell_name(&mut self, name: String) {
76 self.state.shell_name = name;
77 }
78
79 pub fn set_positional_params(&mut self, params: Vec<String>) {
81 self.state.positional_params = params;
82 }
83
84 pub fn fs(&self) -> &Arc<dyn crate::vfs::VirtualFs> {
88 &self.state.fs
89 }
90
91 pub fn write_file(&self, path: &str, content: &[u8]) -> Result<(), crate::VfsError> {
93 let p = Path::new(path);
94 if let Some(parent) = p.parent()
95 && parent != Path::new("/")
96 {
97 self.state.fs.mkdir_p(parent)?;
98 }
99 self.state.fs.write_file(p, content)
100 }
101
102 pub fn read_file(&self, path: &str) -> Result<Vec<u8>, crate::VfsError> {
104 self.state.fs.read_file(Path::new(path))
105 }
106
107 pub fn mkdir(&self, path: &str, recursive: bool) -> Result<(), crate::VfsError> {
109 let p = Path::new(path);
110 if recursive {
111 self.state.fs.mkdir_p(p)
112 } else {
113 self.state.fs.mkdir(p)
114 }
115 }
116
117 pub fn exists(&self, path: &str) -> bool {
119 self.state.fs.exists(Path::new(path))
120 }
121
122 pub fn readdir(&self, path: &str) -> Result<Vec<crate::vfs::DirEntry>, crate::VfsError> {
124 self.state.fs.readdir(Path::new(path))
125 }
126
127 pub fn stat(&self, path: &str) -> Result<crate::vfs::Metadata, crate::VfsError> {
129 self.state.fs.stat(Path::new(path))
130 }
131
132 pub fn remove_file(&self, path: &str) -> Result<(), crate::VfsError> {
134 self.state.fs.remove_file(Path::new(path))
135 }
136
137 pub fn remove_dir_all(&self, path: &str) -> Result<(), crate::VfsError> {
139 self.state.fs.remove_dir_all(Path::new(path))
140 }
141
142 pub fn register_command(&mut self, cmd: Box<dyn VirtualCommand>) {
144 self.state.commands.insert(cmd.name().to_string(), cmd);
145 }
146
147 pub fn exec_with_overrides(
151 &mut self,
152 input: &str,
153 env: Option<&HashMap<String, String>>,
154 cwd: Option<&str>,
155 stdin: Option<&str>,
156 ) -> Result<ExecResult, RustBashError> {
157 let saved_cwd = self.state.cwd.clone();
158 let mut overwritten_env: Vec<(String, Option<Variable>)> = Vec::new();
159
160 if let Some(env) = env {
161 for (key, value) in env {
162 let old = self.state.env.get(key).cloned();
163 overwritten_env.push((key.clone(), old));
164 self.state.env.insert(
165 key.clone(),
166 Variable {
167 value: VariableValue::Scalar(value.clone()),
168 attrs: VariableAttrs::EXPORTED,
169 },
170 );
171 }
172 }
173
174 if let Some(cwd) = cwd {
175 self.state.cwd = cwd.to_string();
176 }
177
178 let result = if let Some(stdin) = stdin {
179 let delimiter = if stdin.contains("__EXEC_STDIN__") {
180 "__EXEC_STDIN_BOUNDARY__"
181 } else {
182 "__EXEC_STDIN__"
183 };
184 let full_command = format!("{input} <<'{delimiter}'\n{stdin}\n{delimiter}");
185 self.exec(&full_command)
186 } else {
187 self.exec(input)
188 };
189
190 self.state.cwd = saved_cwd;
192 for (key, old_val) in overwritten_env {
193 match old_val {
194 Some(var) => {
195 self.state.env.insert(key, var);
196 }
197 None => {
198 self.state.env.remove(&key);
199 }
200 }
201 }
202
203 result
204 }
205
206 pub fn is_input_complete(input: &str) -> bool {
214 match brush_parser::tokenize_str(input) {
215 Err(e) if e.is_incomplete() => false,
216 Err(_) => true, Ok(tokens) => {
218 if tokens.is_empty() {
219 return true;
220 }
221 let options = interpreter::parser_options();
222 let source_info = brush_parser::SourceInfo {
223 source: input.to_string(),
224 };
225 match brush_parser::parse_tokens(&tokens, &options, &source_info) {
226 Ok(_) => true,
227 Err(brush_parser::ParseError::ParsingAtEndOfInput) => false,
228 Err(_) => true, }
230 }
231 }
232 }
233}
234
235pub struct RustBashBuilder {
237 files: HashMap<String, Vec<u8>>,
238 env: HashMap<String, String>,
239 cwd: Option<String>,
240 custom_commands: Vec<Box<dyn VirtualCommand>>,
241 limits: Option<ExecutionLimits>,
242 network_policy: Option<NetworkPolicy>,
243 fs: Option<Arc<dyn VirtualFs>>,
244}
245
246impl Default for RustBashBuilder {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252impl RustBashBuilder {
253 pub fn new() -> Self {
255 Self {
256 files: HashMap::new(),
257 env: HashMap::new(),
258 cwd: None,
259 custom_commands: Vec::new(),
260 limits: None,
261 network_policy: None,
262 fs: None,
263 }
264 }
265
266 pub fn files(mut self, files: HashMap<String, Vec<u8>>) -> Self {
268 self.files = files;
269 self
270 }
271
272 pub fn env(mut self, env: HashMap<String, String>) -> Self {
274 self.env = env;
275 self
276 }
277
278 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
280 self.cwd = Some(cwd.into());
281 self
282 }
283
284 pub fn command(mut self, cmd: Box<dyn VirtualCommand>) -> Self {
286 self.custom_commands.push(cmd);
287 self
288 }
289
290 pub fn execution_limits(mut self, limits: ExecutionLimits) -> Self {
292 self.limits = Some(limits);
293 self
294 }
295
296 pub fn max_array_elements(mut self, max: usize) -> Self {
298 let mut limits = self.limits.unwrap_or_default();
299 limits.max_array_elements = max;
300 self.limits = Some(limits);
301 self
302 }
303
304 pub fn network_policy(mut self, policy: NetworkPolicy) -> Self {
306 self.network_policy = Some(policy);
307 self
308 }
309
310 pub fn fs(mut self, fs: Arc<dyn VirtualFs>) -> Self {
316 self.fs = Some(fs);
317 self
318 }
319
320 pub fn build(self) -> Result<RustBash, RustBashError> {
322 let fs: Arc<dyn VirtualFs> = self.fs.unwrap_or_else(|| Arc::new(InMemoryFs::new()));
323 let cwd = self.cwd.unwrap_or_else(|| "/".to_string());
324 fs.mkdir_p(Path::new(&cwd))?;
325
326 for (path, content) in &self.files {
327 let p = Path::new(path);
328 if let Some(parent) = p.parent()
329 && parent != Path::new("/")
330 {
331 fs.mkdir_p(parent)?;
332 }
333 fs.write_file(p, content)?;
334 }
335
336 let mut commands = commands::register_default_commands();
337 for cmd in self.custom_commands {
338 commands.insert(cmd.name().to_string(), cmd);
339 }
340
341 let mut env_map = self.env;
343 let defaults: &[(&str, &str)] = &[
344 ("PATH", "/usr/bin:/bin"),
345 ("HOME", "/home/user"),
346 ("USER", "user"),
347 ("HOSTNAME", "rust-bash"),
348 ("OSTYPE", "linux-gnu"),
349 ("SHELL", "/bin/bash"),
350 ("BASH", "/bin/bash"),
351 ("BASH_VERSION", env!("CARGO_PKG_VERSION")),
352 ("OLDPWD", ""),
353 ("TERM", "xterm-256color"),
354 ];
355 for &(key, value) in defaults {
356 env_map
357 .entry(key.to_string())
358 .or_insert_with(|| value.to_string());
359 }
360 env_map
361 .entry("PWD".to_string())
362 .or_insert_with(|| cwd.clone());
363
364 setup_default_filesystem(fs.as_ref(), &env_map, &commands)?;
365
366 let mut env: HashMap<String, Variable> = env_map
367 .into_iter()
368 .map(|(k, v)| {
369 (
370 k,
371 Variable {
372 value: VariableValue::Scalar(v),
373 attrs: VariableAttrs::EXPORTED,
374 },
375 )
376 })
377 .collect();
378
379 for (name, val) in &[("OPTIND", "1"), ("OPTERR", "1")] {
381 env.entry(name.to_string()).or_insert_with(|| Variable {
382 value: VariableValue::Scalar(val.to_string()),
383 attrs: VariableAttrs::empty(),
384 });
385 }
386
387 let mut state = InterpreterState {
388 fs,
389 env,
390 cwd,
391 functions: HashMap::new(),
392 last_exit_code: 0,
393 commands,
394 shell_opts: ShellOpts::default(),
395 shopt_opts: ShoptOpts::default(),
396 limits: self.limits.unwrap_or_default(),
397 counters: ExecutionCounters::default(),
398 network_policy: self.network_policy.unwrap_or_default(),
399 should_exit: false,
400 loop_depth: 0,
401 control_flow: None,
402 positional_params: Vec::new(),
403 shell_name: "rust-bash".to_string(),
404 random_seed: 0,
405 local_scopes: Vec::new(),
406 in_function_depth: 0,
407 traps: HashMap::new(),
408 in_trap: false,
409 errexit_suppressed: 0,
410 stdin_offset: 0,
411 dir_stack: Vec::new(),
412 command_hash: HashMap::new(),
413 aliases: HashMap::new(),
414 current_lineno: 0,
415 shell_start_time: Instant::now(),
416 last_argument: String::new(),
417 call_stack: Vec::new(),
418 machtype: "x86_64-pc-linux-gnu".to_string(),
419 hosttype: "x86_64".to_string(),
420 persistent_fds: HashMap::new(),
421 next_auto_fd: 10,
422 proc_sub_counter: 0,
423 proc_sub_prealloc: HashMap::new(),
424 pipe_stdin_bytes: None,
425 };
426
427 state.env.insert(
430 "SHELLOPTS".to_string(),
431 Variable {
432 value: VariableValue::Scalar(String::new()),
433 attrs: VariableAttrs::READONLY,
434 },
435 );
436 state.env.insert(
437 "BASHOPTS".to_string(),
438 Variable {
439 value: VariableValue::Scalar(String::new()),
440 attrs: VariableAttrs::READONLY,
441 },
442 );
443
444 state.env.insert(
446 "PS4".to_string(),
447 Variable {
448 value: VariableValue::Scalar("+ ".to_string()),
449 attrs: VariableAttrs::empty(),
450 },
451 );
452
453 Ok(RustBash { state })
454 }
455}
456
457fn setup_default_filesystem(
462 fs: &dyn VirtualFs,
463 env: &HashMap<String, String>,
464 commands: &HashMap<String, Box<dyn commands::VirtualCommand>>,
465) -> Result<(), RustBashError> {
466 for dir in &["/bin", "/usr/bin", "/tmp", "/dev"] {
468 let _ = fs.mkdir_p(Path::new(dir));
469 }
470
471 if let Some(home) = env.get("HOME") {
473 let _ = fs.mkdir_p(Path::new(home));
474 }
475
476 for name in &["null", "zero", "stdin", "stdout", "stderr"] {
478 let path_str = format!("/dev/{name}");
479 let p = Path::new(&path_str);
480 if !fs.exists(p) {
481 fs.write_file(p, b"")?;
482 }
483 }
484
485 for name in commands.keys() {
487 let path_str = format!("/bin/{name}");
488 let p = Path::new(&path_str);
489 if !fs.exists(p) {
490 let content = format!("#!/bin/bash\n# built-in: {name}\n");
491 fs.write_file(p, content.as_bytes())?;
492 }
493 }
494
495 for &name in interpreter::builtins::builtin_names() {
497 if matches!(name, "." | ":" | "colon") {
498 continue;
499 }
500 let path_str = format!("/bin/{name}");
501 let p = Path::new(&path_str);
502 if !fs.exists(p) {
503 let content = format!("#!/bin/bash\n# built-in: {name}\n");
504 fs.write_file(p, content.as_bytes())?;
505 }
506 }
507
508 Ok(())
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 fn shell() -> RustBash {
516 RustBashBuilder::new().build().unwrap()
517 }
518
519 #[test]
522 fn echo_hello_end_to_end() {
523 let mut shell = shell();
524 let result = shell.exec("echo hello").unwrap();
525 assert_eq!(result.stdout, "hello\n");
526 assert_eq!(result.exit_code, 0);
527 assert_eq!(result.stderr, "");
528 }
529
530 #[test]
533 fn echo_multiple_words() {
534 let mut shell = shell();
535 let result = shell.exec("echo hello world").unwrap();
536 assert_eq!(result.stdout, "hello world\n");
537 }
538
539 #[test]
540 fn echo_no_args() {
541 let mut shell = shell();
542 let result = shell.exec("echo").unwrap();
543 assert_eq!(result.stdout, "\n");
544 }
545
546 #[test]
547 fn echo_no_newline() {
548 let mut shell = shell();
549 let result = shell.exec("echo -n hello").unwrap();
550 assert_eq!(result.stdout, "hello");
551 }
552
553 #[test]
554 fn echo_escape_sequences() {
555 let mut shell = shell();
556 let result = shell.exec(r"echo -e 'hello\nworld'").unwrap();
557 assert_eq!(result.stdout, "hello\nworld\n");
558 }
559
560 #[test]
563 fn true_command() {
564 let mut shell = shell();
565 let result = shell.exec("true").unwrap();
566 assert_eq!(result.exit_code, 0);
567 assert_eq!(result.stdout, "");
568 }
569
570 #[test]
571 fn false_command() {
572 let mut shell = shell();
573 let result = shell.exec("false").unwrap();
574 assert_eq!(result.exit_code, 1);
575 }
576
577 #[test]
580 fn exit_default_code() {
581 let mut shell = shell();
582 let result = shell.exec("exit").unwrap();
583 assert_eq!(result.exit_code, 0);
584 }
585
586 #[test]
587 fn exit_with_code() {
588 let mut shell = shell();
589 let result = shell.exec("exit 42").unwrap();
590 assert_eq!(result.exit_code, 42);
591 }
592
593 #[test]
594 fn exit_stops_subsequent_commands() {
595 let mut shell = shell();
596 let result = shell.exec("exit 1; echo should_not_appear").unwrap();
597 assert_eq!(result.exit_code, 1);
598 assert!(!result.stdout.contains("should_not_appear"));
599 }
600
601 #[test]
602 fn exit_non_numeric_argument() {
603 let mut shell = shell();
604 let result = shell.exec("exit foo").unwrap();
605 assert_eq!(result.exit_code, 2);
606 assert!(result.stderr.contains("numeric argument required"));
607 }
608
609 #[test]
612 fn command_not_found() {
613 let mut shell = shell();
614 let result = shell.exec("nonexistent_cmd").unwrap();
615 assert_eq!(result.exit_code, 127);
616 assert!(result.stderr.contains("command not found"));
617 }
618
619 #[test]
622 fn sequential_commands() {
623 let mut shell = shell();
624 let result = shell.exec("echo hello; echo world").unwrap();
625 assert_eq!(result.stdout, "hello\nworld\n");
626 }
627
628 #[test]
629 fn sequential_exit_code_is_last() {
630 let mut shell = shell();
631 let result = shell.exec("true; false").unwrap();
632 assert_eq!(result.exit_code, 1);
633 }
634
635 #[test]
638 fn and_success() {
639 let mut shell = shell();
640 let result = shell.exec("true && echo yes").unwrap();
641 assert_eq!(result.stdout, "yes\n");
642 }
643
644 #[test]
645 fn and_failure_skips() {
646 let mut shell = shell();
647 let result = shell.exec("false && echo yes").unwrap();
648 assert_eq!(result.stdout, "");
649 assert_eq!(result.exit_code, 1);
650 }
651
652 #[test]
653 fn or_success_skips() {
654 let mut shell = shell();
655 let result = shell.exec("true || echo no").unwrap();
656 assert_eq!(result.stdout, "");
657 assert_eq!(result.exit_code, 0);
658 }
659
660 #[test]
661 fn or_failure_runs() {
662 let mut shell = shell();
663 let result = shell.exec("false || echo yes").unwrap();
664 assert_eq!(result.stdout, "yes\n");
665 assert_eq!(result.exit_code, 0);
666 }
667
668 #[test]
669 fn chained_and_or() {
670 let mut shell = shell();
671 let result = shell.exec("false || true && echo yes").unwrap();
672 assert_eq!(result.stdout, "yes\n");
673 assert_eq!(result.exit_code, 0);
674 }
675
676 #[test]
679 fn pipeline_negation_true() {
680 let mut shell = shell();
681 let result = shell.exec("! true").unwrap();
682 assert_eq!(result.exit_code, 1);
683 }
684
685 #[test]
686 fn pipeline_negation_false() {
687 let mut shell = shell();
688 let result = shell.exec("! false").unwrap();
689 assert_eq!(result.exit_code, 0);
690 }
691
692 #[test]
695 fn bare_assignment() {
696 let mut shell = shell();
697 let result = shell.exec("FOO=bar").unwrap();
698 assert_eq!(result.exit_code, 0);
699 assert_eq!(shell.state.env.get("FOO").unwrap().value.as_scalar(), "bar");
700 }
701
702 #[test]
705 fn state_persists_across_exec_calls() {
706 let mut shell = shell();
707 shell.exec("FOO=hello").unwrap();
708 assert_eq!(
709 shell.state.env.get("FOO").unwrap().value.as_scalar(),
710 "hello"
711 );
712 let result = shell.exec("true").unwrap();
713 assert_eq!(result.exit_code, 0);
714 assert_eq!(
715 shell.state.env.get("FOO").unwrap().value.as_scalar(),
716 "hello"
717 );
718 }
719
720 #[test]
723 fn empty_input() {
724 let mut shell = shell();
725 let result = shell.exec("").unwrap();
726 assert_eq!(result.stdout, "");
727 assert_eq!(result.exit_code, 0);
728 }
729
730 #[test]
731 fn whitespace_only_input() {
732 let mut shell = shell();
733 let result = shell.exec(" ").unwrap();
734 assert_eq!(result.stdout, "");
735 assert_eq!(result.exit_code, 0);
736 }
737
738 #[test]
741 fn builder_default_cwd() {
742 let shell = RustBashBuilder::new().build().unwrap();
743 assert_eq!(shell.state.cwd, "/");
744 }
745
746 #[test]
747 fn builder_with_cwd() {
748 let shell = RustBashBuilder::new().cwd("/home/user").build().unwrap();
749 assert_eq!(shell.state.cwd, "/home/user");
750 }
751
752 #[test]
753 fn builder_with_env() {
754 let mut env = HashMap::new();
755 env.insert("HOME".to_string(), "/home/test".to_string());
756 let shell = RustBashBuilder::new().env(env).build().unwrap();
757 assert_eq!(
758 shell.state.env.get("HOME").unwrap().value.as_scalar(),
759 "/home/test"
760 );
761 }
762
763 #[test]
764 fn builder_with_files() {
765 let mut files = HashMap::new();
766 files.insert("/etc/test.txt".to_string(), b"hello".to_vec());
767 let shell = RustBashBuilder::new().files(files).build().unwrap();
768 let content = shell
769 .state
770 .fs
771 .read_file(Path::new("/etc/test.txt"))
772 .unwrap();
773 assert_eq!(content, b"hello");
774 }
775
776 #[test]
777 fn builder_with_custom_command() {
778 use crate::commands::{CommandContext, CommandResult, VirtualCommand};
779
780 struct CustomCmd;
781 impl VirtualCommand for CustomCmd {
782 fn name(&self) -> &str {
783 "custom"
784 }
785 fn execute(&self, _args: &[String], _ctx: &CommandContext) -> CommandResult {
786 CommandResult {
787 stdout: "custom output\n".to_string(),
788 ..CommandResult::default()
789 }
790 }
791 }
792
793 let mut shell = RustBashBuilder::new()
794 .command(Box::new(CustomCmd))
795 .build()
796 .unwrap();
797 let result = shell.exec("custom").unwrap();
798 assert_eq!(result.stdout, "custom output\n");
799 }
800
801 #[test]
804 fn exit_wraps_to_byte_range() {
805 let mut shell = shell();
806 let result = shell.exec("exit 256").unwrap();
807 assert_eq!(result.exit_code, 0);
808 }
809
810 #[test]
811 fn multiple_bare_assignments() {
812 let mut shell = shell();
813 shell.exec("A=1 B=2").unwrap();
814 assert_eq!(shell.state.env.get("A").unwrap().value.as_scalar(), "1");
815 assert_eq!(shell.state.env.get("B").unwrap().value.as_scalar(), "2");
816 }
817
818 #[test]
819 fn comment_stripping() {
820 let mut shell = shell();
821 let result = shell.exec("echo hello # this is a comment").unwrap();
822 assert_eq!(result.stdout, "hello\n");
823 }
824
825 #[test]
826 fn negation_with_and_or() {
827 let mut shell = shell();
828 let result = shell.exec("! false && echo yes").unwrap();
829 assert_eq!(result.stdout, "yes\n");
830 assert_eq!(result.exit_code, 0);
831 }
832
833 #[test]
834 fn deeply_chained_and_or() {
835 let mut shell = shell();
836 let result = shell.exec("true && false || true && echo yes").unwrap();
837 assert_eq!(result.stdout, "yes\n");
838 assert_eq!(result.exit_code, 0);
839 }
840
841 #[test]
844 fn expand_simple_variable() {
845 let mut shell = shell();
846 shell.exec("FOO=bar").unwrap();
847 let result = shell.exec("echo $FOO").unwrap();
848 assert_eq!(result.stdout, "bar\n");
849 }
850
851 #[test]
852 fn expand_braced_variable() {
853 let mut shell = shell();
854 shell.exec("FOO=bar").unwrap();
855 let result = shell.exec("echo ${FOO}").unwrap();
856 assert_eq!(result.stdout, "bar\n");
857 }
858
859 #[test]
860 fn expand_unset_variable_is_empty() {
861 let mut shell = shell();
862 let result = shell.exec("echo \"$UNDEFINED\"").unwrap();
863 assert_eq!(result.stdout, "\n");
864 }
865
866 #[test]
867 fn expand_default_value() {
868 let mut shell = shell();
869 let result = shell.exec("echo ${UNSET:-default}").unwrap();
870 assert_eq!(result.stdout, "default\n");
871 }
872
873 #[test]
874 fn expand_default_not_used_when_set() {
875 let mut shell = shell();
876 shell.exec("VAR=hello").unwrap();
877 let result = shell.exec("echo ${VAR:-default}").unwrap();
878 assert_eq!(result.stdout, "hello\n");
879 }
880
881 #[test]
882 fn expand_assign_default() {
883 let mut shell = shell();
884 let result = shell.exec("echo ${UNSET:=fallback}").unwrap();
885 assert_eq!(result.stdout, "fallback\n");
886 assert_eq!(
887 shell.state.env.get("UNSET").unwrap().value.as_scalar(),
888 "fallback"
889 );
890 }
891
892 #[test]
893 fn expand_default_with_variable() {
894 let mut shell = shell();
895 shell.exec("FALLBACK=resolved").unwrap();
896 let result = shell.exec("echo ${UNSET:-$FALLBACK}").unwrap();
897 assert_eq!(result.stdout, "resolved\n");
898 }
899
900 #[test]
901 fn expand_error_if_unset() {
902 let mut shell = shell();
903 let result = shell.exec("echo ${UNSET:?missing var}").unwrap();
904 assert_eq!(result.exit_code, 127);
905 assert!(result.stderr.contains("missing var"));
906 assert!(result.stdout.is_empty());
907 }
908
909 #[test]
910 fn expand_alternative_value() {
911 let mut shell = shell();
912 shell.exec("VAR=hello").unwrap();
913 let result = shell.exec("echo ${VAR:+alt}").unwrap();
914 assert_eq!(result.stdout, "alt\n");
915 }
916
917 #[test]
918 fn expand_alternative_unset_is_empty() {
919 let mut shell = shell();
920 let result = shell.exec("echo \"${UNSET:+alt}\"").unwrap();
921 assert_eq!(result.stdout, "\n");
922 }
923
924 #[test]
925 fn expand_string_length() {
926 let mut shell = shell();
927 shell.exec("VAR=hello").unwrap();
928 let result = shell.exec("echo ${#VAR}").unwrap();
929 assert_eq!(result.stdout, "5\n");
930 }
931
932 #[test]
933 fn expand_suffix_removal_shortest() {
934 let mut shell = shell();
935 shell.exec("FILE=hello.tar.gz").unwrap();
936 let result = shell.exec("echo ${FILE%.*}").unwrap();
937 assert_eq!(result.stdout, "hello.tar\n");
938 }
939
940 #[test]
941 fn expand_suffix_removal_longest() {
942 let mut shell = shell();
943 shell.exec("FILE=hello.tar.gz").unwrap();
944 let result = shell.exec("echo ${FILE%%.*}").unwrap();
945 assert_eq!(result.stdout, "hello\n");
946 }
947
948 #[test]
949 fn expand_prefix_removal_shortest() {
950 let mut shell = shell();
951 shell.exec("PATH_VAR=/a/b/c").unwrap();
952 let result = shell.exec("echo ${PATH_VAR#*/}").unwrap();
953 assert_eq!(result.stdout, "a/b/c\n");
954 }
955
956 #[test]
957 fn expand_prefix_removal_longest() {
958 let mut shell = shell();
959 shell.exec("PATH_VAR=/a/b/c").unwrap();
960 let result = shell.exec("echo ${PATH_VAR##*/}").unwrap();
961 assert_eq!(result.stdout, "c\n");
962 }
963
964 #[test]
965 fn expand_substitution_first() {
966 let mut shell = shell();
967 shell.exec("STR=hello").unwrap();
968 let result = shell.exec("echo ${STR/l/r}").unwrap();
969 assert_eq!(result.stdout, "herlo\n");
970 }
971
972 #[test]
973 fn expand_substitution_all() {
974 let mut shell = shell();
975 shell.exec("STR=hello").unwrap();
976 let result = shell.exec("echo ${STR//l/r}").unwrap();
977 assert_eq!(result.stdout, "herro\n");
978 }
979
980 #[test]
981 fn expand_substring() {
982 let mut shell = shell();
983 shell.exec("STR=hello").unwrap();
984 let result = shell.exec("echo ${STR:1:3}").unwrap();
985 assert_eq!(result.stdout, "ell\n");
986 }
987
988 #[test]
989 fn expand_uppercase_first() {
990 let mut shell = shell();
991 shell.exec("STR=hello").unwrap();
992 let result = shell.exec("echo ${STR^}").unwrap();
993 assert_eq!(result.stdout, "Hello\n");
994 }
995
996 #[test]
997 fn expand_uppercase_all() {
998 let mut shell = shell();
999 shell.exec("STR=hello").unwrap();
1000 let result = shell.exec("echo ${STR^^}").unwrap();
1001 assert_eq!(result.stdout, "HELLO\n");
1002 }
1003
1004 #[test]
1005 fn expand_lowercase_first() {
1006 let mut shell = shell();
1007 shell.exec("STR=HELLO").unwrap();
1008 let result = shell.exec("echo ${STR,}").unwrap();
1009 assert_eq!(result.stdout, "hELLO\n");
1010 }
1011
1012 #[test]
1013 fn expand_lowercase_all() {
1014 let mut shell = shell();
1015 shell.exec("STR=HELLO").unwrap();
1016 let result = shell.exec("echo ${STR,,}").unwrap();
1017 assert_eq!(result.stdout, "hello\n");
1018 }
1019
1020 #[test]
1023 fn expand_exit_status() {
1024 let mut shell = shell();
1025 shell.exec("false").unwrap();
1026 let result = shell.exec("echo $?").unwrap();
1027 assert_eq!(result.stdout, "1\n");
1028 }
1029
1030 #[test]
1031 fn expand_dollar_dollar() {
1032 let mut shell = shell();
1033 let result = shell.exec("echo $$").unwrap();
1034 assert_eq!(result.stdout, "1\n");
1035 }
1036
1037 #[test]
1038 fn expand_dollar_zero() {
1039 let mut shell = shell();
1040 let result = shell.exec("echo $0").unwrap();
1041 assert_eq!(result.stdout, "rust-bash\n");
1042 }
1043
1044 #[test]
1045 fn expand_positional_params() {
1046 let mut shell = shell();
1047 shell.exec("set -- a b c").unwrap();
1048 let result = shell.exec("echo $1 $2 $3").unwrap();
1049 assert_eq!(result.stdout, "a b c\n");
1050 }
1051
1052 #[test]
1053 fn expand_param_count() {
1054 let mut shell = shell();
1055 shell.exec("set -- a b c").unwrap();
1056 let result = shell.exec("echo $#").unwrap();
1057 assert_eq!(result.stdout, "3\n");
1058 }
1059
1060 #[test]
1061 fn expand_at_all_params() {
1062 let mut shell = shell();
1063 shell.exec("set -- one two three").unwrap();
1064 let result = shell.exec("echo $@").unwrap();
1065 assert_eq!(result.stdout, "one two three\n");
1066 }
1067
1068 #[test]
1069 fn expand_star_all_params() {
1070 let mut shell = shell();
1071 shell.exec("set -- one two three").unwrap();
1072 let result = shell.exec("echo $*").unwrap();
1073 assert_eq!(result.stdout, "one two three\n");
1074 }
1075
1076 #[test]
1077 fn expand_random_is_numeric() {
1078 let mut shell = shell();
1079 let result = shell.exec("echo $RANDOM").unwrap();
1080 let val: u32 = result.stdout.trim().parse().unwrap();
1081 assert!(val <= 32767);
1082 }
1083
1084 #[test]
1087 fn tilde_expands_to_home() {
1088 let mut env = HashMap::new();
1089 env.insert("HOME".to_string(), "/home/test".to_string());
1090 let mut shell = RustBashBuilder::new().env(env).build().unwrap();
1091 let result = shell.exec("echo ~").unwrap();
1092 assert_eq!(result.stdout, "/home/test\n");
1093 }
1094
1095 #[test]
1098 fn redirect_stdout_to_file() {
1099 let mut shell = shell();
1100 shell.exec("echo hello > /output.txt").unwrap();
1101 let content = shell.state.fs.read_file(Path::new("/output.txt")).unwrap();
1102 assert_eq!(String::from_utf8_lossy(&content), "hello\n");
1103 }
1104
1105 #[test]
1106 fn redirect_append() {
1107 let mut shell = shell();
1108 shell.exec("echo hello > /output.txt").unwrap();
1109 shell.exec("echo world >> /output.txt").unwrap();
1110 let content = shell.state.fs.read_file(Path::new("/output.txt")).unwrap();
1111 assert_eq!(String::from_utf8_lossy(&content), "hello\nworld\n");
1112 }
1113
1114 #[test]
1115 fn redirect_stdin_from_file() {
1116 let mut files = HashMap::new();
1117 files.insert("/input.txt".to_string(), b"file contents\n".to_vec());
1118 let mut shell = RustBashBuilder::new().files(files).build().unwrap();
1119 let result = shell.exec("cat < /input.txt").unwrap();
1120 assert_eq!(result.stdout, "file contents\n");
1121 }
1122
1123 #[test]
1124 fn redirect_stderr_to_file() {
1125 let mut shell = shell();
1126 shell.exec("nonexistent 2> /err.txt").unwrap();
1127 let content = shell.state.fs.read_file(Path::new("/err.txt")).unwrap();
1128 assert!(String::from_utf8_lossy(&content).contains("command not found"));
1129 }
1130
1131 #[test]
1132 fn redirect_dev_null() {
1133 let mut shell = shell();
1134 let result = shell.exec("echo hello > /dev/null").unwrap();
1135 assert_eq!(result.stdout, "");
1136 }
1137
1138 #[test]
1139 fn redirect_stderr_to_stdout() {
1140 let mut shell = shell();
1141 let result = shell.exec("nonexistent 2>&1").unwrap();
1142 assert!(result.stdout.contains("command not found"));
1143 assert_eq!(result.stderr, "");
1144 }
1145
1146 #[test]
1147 fn redirect_write_then_cat() {
1148 let mut shell = shell();
1149 shell.exec("echo hello > /test.txt").unwrap();
1150 let result = shell.exec("cat /test.txt").unwrap();
1151 assert_eq!(result.stdout, "hello\n");
1152 }
1153
1154 #[test]
1157 fn cat_stdin() {
1158 let mut shell = shell();
1159 let result = shell.exec("echo hello | cat").unwrap();
1160 assert_eq!(result.stdout, "hello\n");
1161 }
1162
1163 #[test]
1164 fn cat_file() {
1165 let mut files = HashMap::new();
1166 files.insert("/test.txt".to_string(), b"content\n".to_vec());
1167 let mut shell = RustBashBuilder::new().files(files).build().unwrap();
1168 let result = shell.exec("cat /test.txt").unwrap();
1169 assert_eq!(result.stdout, "content\n");
1170 }
1171
1172 #[test]
1173 fn cat_nonexistent_file() {
1174 let mut shell = shell();
1175 let result = shell.exec("cat /no_such_file.txt").unwrap();
1176 assert_eq!(result.exit_code, 1);
1177 assert!(result.stderr.contains("No such file"));
1178 }
1179
1180 #[test]
1181 fn cat_line_numbers() {
1182 let mut files = HashMap::new();
1183 files.insert("/test.txt".to_string(), b"a\nb\nc\n".to_vec());
1184 let mut shell = RustBashBuilder::new().files(files).build().unwrap();
1185 let result = shell.exec("cat -n /test.txt").unwrap();
1186 assert!(result.stdout.contains("1\ta"));
1187 assert!(result.stdout.contains("2\tb"));
1188 assert!(result.stdout.contains("3\tc"));
1189 }
1190
1191 #[test]
1194 fn cd_changes_cwd() {
1195 let mut shell = RustBashBuilder::new().cwd("/home/user").build().unwrap();
1196 shell.exec("cd /").unwrap();
1197 assert_eq!(shell.state.cwd, "/");
1198 }
1199
1200 #[test]
1201 fn cd_home() {
1202 let mut env = HashMap::new();
1203 env.insert("HOME".to_string(), "/home/test".to_string());
1204 let mut shell = RustBashBuilder::new()
1205 .cwd("/home/test")
1206 .env(env)
1207 .build()
1208 .unwrap();
1209 shell.exec("cd /").unwrap();
1210 shell.exec("cd").unwrap();
1211 assert_eq!(shell.state.cwd, "/home/test");
1212 }
1213
1214 #[test]
1215 fn cd_sets_oldpwd() {
1216 let mut shell = RustBashBuilder::new().cwd("/home/user").build().unwrap();
1217 shell.exec("cd /").unwrap();
1218 assert_eq!(
1219 shell.state.env.get("OLDPWD").unwrap().value.as_scalar(),
1220 "/home/user"
1221 );
1222 }
1223
1224 #[test]
1225 fn export_creates_exported_var() {
1226 let mut shell = shell();
1227 shell.exec("export FOO=bar").unwrap();
1228 let var = shell.state.env.get("FOO").unwrap();
1229 assert_eq!(var.value.as_scalar(), "bar");
1230 assert!(var.exported());
1231 }
1232
1233 #[test]
1234 fn export_marks_existing_var() {
1235 let mut shell = shell();
1236 shell.exec("FOO=bar").unwrap();
1237 assert!(!shell.state.env.get("FOO").unwrap().exported());
1238 shell.exec("export FOO").unwrap();
1239 assert!(shell.state.env.get("FOO").unwrap().exported());
1240 }
1241
1242 #[test]
1243 fn unset_removes_var() {
1244 let mut shell = shell();
1245 shell.exec("FOO=bar").unwrap();
1246 shell.exec("unset FOO").unwrap();
1247 assert!(!shell.state.env.contains_key("FOO"));
1248 }
1249
1250 #[test]
1251 fn set_options() {
1252 let mut shell = shell();
1253 shell.exec("set -e").unwrap();
1254 assert!(shell.state.shell_opts.errexit);
1255 shell.exec("set +e").unwrap();
1256 assert!(!shell.state.shell_opts.errexit);
1257 }
1258
1259 #[test]
1260 fn set_positional_params() {
1261 let mut shell = shell();
1262 shell.exec("set -- x y z").unwrap();
1263 assert_eq!(shell.state.positional_params, vec!["x", "y", "z"]);
1264 }
1265
1266 #[test]
1267 fn shift_positional_params() {
1268 let mut shell = shell();
1269 shell.exec("set -- a b c d").unwrap();
1270 shell.exec("shift 2").unwrap();
1271 assert_eq!(shell.state.positional_params, vec!["c", "d"]);
1272 }
1273
1274 #[test]
1275 fn readonly_variable() {
1276 let mut shell = shell();
1277 shell.exec("readonly X=42").unwrap();
1278 let var = shell.state.env.get("X").unwrap();
1279 assert_eq!(var.value.as_scalar(), "42");
1280 assert!(var.readonly());
1281 let result = shell.exec("X=new").unwrap();
1283 assert_eq!(result.exit_code, 1);
1284 assert!(result.stderr.contains("readonly"));
1285 assert_eq!(shell.state.env.get("X").unwrap().value.as_scalar(), "42");
1287 }
1288
1289 #[test]
1290 fn declare_readonly() {
1291 let mut shell = shell();
1292 shell.exec("declare -r Y=99").unwrap();
1293 assert!(shell.state.env.get("Y").unwrap().readonly());
1294 }
1295
1296 #[test]
1297 fn read_from_stdin() {
1298 let mut shell = shell();
1299 shell.exec("echo 'hello world' > /tmp_input").unwrap();
1300 let result = shell.exec("read VAR < /tmp_input").unwrap();
1301 assert_eq!(result.exit_code, 0);
1302 assert_eq!(
1303 shell.state.env.get("VAR").unwrap().value.as_scalar(),
1304 "hello world"
1305 );
1306 }
1307
1308 #[test]
1309 fn read_multiple_vars() {
1310 let mut shell = shell();
1311 shell
1312 .exec("echo 'one two three four' > /tmp_input")
1313 .unwrap();
1314 shell.exec("read A B < /tmp_input").unwrap();
1315 assert_eq!(shell.state.env.get("A").unwrap().value.as_scalar(), "one");
1316 assert_eq!(
1317 shell.state.env.get("B").unwrap().value.as_scalar(),
1318 "two three four"
1319 );
1320 }
1321
1322 #[test]
1323 fn colon_builtin() {
1324 let mut shell = shell();
1325 let result = shell.exec(":").unwrap();
1326 assert_eq!(result.exit_code, 0);
1327 }
1328
1329 #[test]
1332 fn variable_in_redirect_target() {
1333 let mut shell = shell();
1334 shell.exec("FILE=/output.txt").unwrap();
1335 shell.exec("echo hello > $FILE").unwrap();
1336 let content = shell.state.fs.read_file(Path::new("/output.txt")).unwrap();
1337 assert_eq!(String::from_utf8_lossy(&content), "hello\n");
1338 }
1339
1340 #[test]
1341 fn pipeline_with_variable() {
1342 let mut shell = shell();
1343 shell.exec("MSG=world").unwrap();
1344 let result = shell.exec("echo hello $MSG | cat").unwrap();
1345 assert_eq!(result.stdout, "hello world\n");
1346 }
1347
1348 #[test]
1349 fn set_and_expand_positional() {
1350 let mut shell = shell();
1351 shell.exec("set -- foo bar baz").unwrap();
1352 let result = shell.exec("echo $1 $3").unwrap();
1353 assert_eq!(result.stdout, "foo baz\n");
1354 }
1355
1356 #[test]
1357 fn shift_and_expand() {
1358 let mut shell = shell();
1359 shell.exec("set -- a b c").unwrap();
1360 shell.exec("shift").unwrap();
1361 let result = shell.exec("echo $1 $#").unwrap();
1362 assert_eq!(result.stdout, "b 2\n");
1363 }
1364
1365 #[test]
1366 fn set_pipefail_option() {
1367 let mut shell = shell();
1368 shell.exec("set -o pipefail").unwrap();
1369 assert!(shell.state.shell_opts.pipefail);
1370 }
1371
1372 #[test]
1373 fn double_quoted_variable_expansion() {
1374 let mut sh = shell();
1375 sh.exec("FOO='hello world'").unwrap();
1376 let result = sh.exec("echo \"$FOO\"").unwrap();
1377 assert_eq!(result.stdout, "hello world\n");
1378 }
1379
1380 #[test]
1381 fn empty_variable_in_quotes() {
1382 let mut shell = shell();
1383 let result = shell.exec("echo \"$EMPTY\"").unwrap();
1384 assert_eq!(result.stdout, "\n");
1385 }
1386
1387 #[test]
1388 fn here_string() {
1389 let mut shell = shell();
1390 let result = shell.exec("cat <<< 'hello world'").unwrap();
1391 assert_eq!(result.stdout, "hello world\n");
1392 }
1393
1394 #[test]
1395 fn output_and_error_redirect() {
1396 let mut shell = shell();
1397 shell.exec("echo hello &> /both.txt").unwrap();
1398 let content = shell.state.fs.read_file(Path::new("/both.txt")).unwrap();
1399 assert_eq!(String::from_utf8_lossy(&content), "hello\n");
1400 }
1401
1402 #[test]
1405 fn if_then_true() {
1406 let mut shell = shell();
1407 let result = shell
1408 .exec("if true; then echo yes; else echo no; fi")
1409 .unwrap();
1410 assert_eq!(result.stdout, "yes\n");
1411 assert_eq!(result.exit_code, 0);
1412 }
1413
1414 #[test]
1415 fn if_then_false() {
1416 let mut shell = shell();
1417 let result = shell
1418 .exec("if false; then echo yes; else echo no; fi")
1419 .unwrap();
1420 assert_eq!(result.stdout, "no\n");
1421 }
1422
1423 #[test]
1424 fn if_elif_else() {
1425 let mut shell = shell();
1426 let result = shell
1427 .exec("if false; then echo a; elif true; then echo b; else echo c; fi")
1428 .unwrap();
1429 assert_eq!(result.stdout, "b\n");
1430 }
1431
1432 #[test]
1433 fn if_elif_falls_through_to_else() {
1434 let mut shell = shell();
1435 let result = shell
1436 .exec("if false; then echo a; elif false; then echo b; else echo c; fi")
1437 .unwrap();
1438 assert_eq!(result.stdout, "c\n");
1439 }
1440
1441 #[test]
1442 fn if_no_else_unmatched() {
1443 let mut shell = shell();
1444 let result = shell.exec("if false; then echo yes; fi").unwrap();
1445 assert_eq!(result.stdout, "");
1446 assert_eq!(result.exit_code, 0);
1447 }
1448
1449 #[test]
1450 fn if_with_command_condition() {
1451 let mut shell = shell();
1452 shell.exec("X=hello").unwrap();
1453 let result = shell
1454 .exec("if echo checking > /dev/null; then echo passed; fi")
1455 .unwrap();
1456 assert_eq!(result.stdout, "passed\n");
1457 }
1458
1459 #[test]
1460 fn for_loop_basic() {
1461 let mut shell = shell();
1462 let result = shell.exec("for i in a b c; do echo $i; done").unwrap();
1463 assert_eq!(result.stdout, "a\nb\nc\n");
1464 }
1465
1466 #[test]
1467 fn for_loop_with_variable_expansion() {
1468 let mut shell = shell();
1469 let result = shell.exec("for i in x y z; do echo $i; done").unwrap();
1472 assert_eq!(result.stdout, "x\ny\nz\n");
1473 }
1474
1475 #[test]
1476 fn for_loop_variable_persists_after_loop() {
1477 let mut shell = shell();
1478 shell.exec("for i in a b c; do true; done").unwrap();
1479 let result = shell.exec("echo $i").unwrap();
1480 assert_eq!(result.stdout, "c\n");
1481 }
1482
1483 #[test]
1484 fn while_loop_basic() {
1485 let mut shell = shell();
1486 let result = shell
1488 .exec("while false; do echo should-not-appear; done")
1489 .unwrap();
1490 assert_eq!(result.stdout, "");
1491 }
1492
1493 #[test]
1494 fn while_loop_executes_body() {
1495 let mut shell = shell();
1496 let _result = shell.exec(
1500 r#"X=yes; while echo $X > /dev/null && [ "$X" = yes ]; do echo looped; X=no; done"#,
1501 );
1502 }
1503
1504 #[test]
1505 fn until_loop_basic() {
1506 let mut shell = shell();
1507 let result = shell
1508 .exec("until true; do echo should-not-run; done")
1509 .unwrap();
1510 assert_eq!(result.stdout, "");
1511 }
1512
1513 #[test]
1514 fn until_loop_runs_once_when_condition_false() {
1515 let mut shell = shell();
1516 let result = shell.exec("until true; do echo nope; done").unwrap();
1518 assert_eq!(result.stdout, "");
1519 }
1520
1521 #[test]
1522 fn brace_group_basic() {
1523 let mut shell = shell();
1524 let result = shell.exec("{ echo hello; echo world; }").unwrap();
1525 assert_eq!(result.stdout, "hello\nworld\n");
1526 }
1527
1528 #[test]
1529 fn brace_group_shares_scope() {
1530 let mut shell = shell();
1531 shell.exec("X=before").unwrap();
1532 shell.exec("{ X=after; }").unwrap();
1533 let result = shell.exec("echo $X").unwrap();
1534 assert_eq!(result.stdout, "after\n");
1535 }
1536
1537 #[test]
1538 fn subshell_basic() {
1539 let mut shell = shell();
1540 let result = shell.exec("(echo hello)").unwrap();
1541 assert_eq!(result.stdout, "hello\n");
1542 }
1543
1544 #[test]
1545 fn subshell_isolates_variables() {
1546 let mut shell = shell();
1547 let result = shell.exec("X=outer; (X=inner; echo $X); echo $X").unwrap();
1548 assert_eq!(result.stdout, "inner\nouter\n");
1549 }
1550
1551 #[test]
1552 fn subshell_isolates_cwd() {
1553 let mut shell = shell();
1554 shell.exec("mkdir /tmp").unwrap();
1555 let result = shell.exec("(cd /tmp && pwd); pwd").unwrap();
1556 assert_eq!(result.stdout, "/tmp\n/\n");
1557 }
1558
1559 #[test]
1560 fn subshell_propagates_exit_code() {
1561 let mut shell = shell();
1562 let result = shell.exec("(false)").unwrap();
1563 assert_eq!(result.exit_code, 1);
1564 }
1565
1566 #[test]
1567 fn subshell_isolates_fs_writes() {
1568 let mut shell = shell();
1569 shell.exec("(echo data > /subshell_file.txt)").unwrap();
1570 let exists = shell.state.fs.exists(Path::new("/subshell_file.txt"));
1572 assert!(!exists);
1573 }
1574
1575 #[test]
1576 fn nested_if_in_for() {
1577 let mut shell = shell();
1578 let result = shell
1579 .exec("for x in yes no yes; do if true; then echo $x; fi; done")
1580 .unwrap();
1581 assert_eq!(result.stdout, "yes\nno\nyes\n");
1582 }
1583
1584 #[test]
1585 fn compound_command_with_redirect() {
1586 let mut shell = shell();
1587 shell
1588 .exec("{ echo hello; echo world; } > /out.txt")
1589 .unwrap();
1590 let content = shell.state.fs.read_file(Path::new("/out.txt")).unwrap();
1591 assert_eq!(String::from_utf8_lossy(&content), "hello\nworld\n");
1592 }
1593
1594 #[test]
1595 fn for_loop_in_pipeline() {
1596 let mut shell = shell();
1597 let result = shell
1598 .exec("for i in a b c; do echo $i; done | cat")
1599 .unwrap();
1600 assert_eq!(result.stdout, "a\nb\nc\n");
1601 }
1602
1603 #[test]
1604 fn if_in_pipeline() {
1605 let mut shell = shell();
1606 let result = shell.exec("if true; then echo yes; fi | cat").unwrap();
1607 assert_eq!(result.stdout, "yes\n");
1608 }
1609
1610 #[test]
1613 fn touch_creates_file() {
1614 let mut shell = shell();
1615 shell.exec("touch /newfile.txt").unwrap();
1616 assert!(shell.state.fs.exists(Path::new("/newfile.txt")));
1617 let content = shell.state.fs.read_file(Path::new("/newfile.txt")).unwrap();
1618 assert!(content.is_empty());
1619 }
1620
1621 #[test]
1622 fn touch_existing_file_no_error() {
1623 let mut shell = shell();
1624 shell.exec("echo data > /existing.txt").unwrap();
1625 let result = shell.exec("touch /existing.txt").unwrap();
1626 assert_eq!(result.exit_code, 0);
1627 let content = shell
1629 .state
1630 .fs
1631 .read_file(Path::new("/existing.txt"))
1632 .unwrap();
1633 assert_eq!(String::from_utf8_lossy(&content), "data\n");
1634 }
1635
1636 #[test]
1637 fn touch_and_ls() {
1638 let mut shell = shell();
1639 shell.exec("touch /file.txt").unwrap();
1640 let result = shell.exec("ls /").unwrap();
1641 assert!(result.stdout.contains("file.txt"));
1642 }
1643
1644 #[test]
1645 fn mkdir_creates_directory() {
1646 let mut shell = shell();
1647 let result = shell.exec("mkdir /mydir").unwrap();
1648 assert_eq!(result.exit_code, 0);
1649 assert!(shell.state.fs.exists(Path::new("/mydir")));
1650 }
1651
1652 #[test]
1653 fn mkdir_p_creates_parents() {
1654 let mut shell = shell();
1655 let result = shell.exec("mkdir -p /a/b/c").unwrap();
1656 assert_eq!(result.exit_code, 0);
1657 assert!(shell.state.fs.exists(Path::new("/a/b/c")));
1658 }
1659
1660 #[test]
1661 fn mkdir_p_and_ls() {
1662 let mut shell = shell();
1663 shell.exec("mkdir -p /a/b/c").unwrap();
1664 let result = shell.exec("ls /a/b").unwrap();
1665 assert!(result.stdout.contains("c"));
1666 }
1667
1668 #[test]
1669 fn ls_root_empty() {
1670 let mut shell = shell();
1671 let result = shell.exec("ls /").unwrap();
1672 assert_eq!(result.exit_code, 0);
1673 }
1674
1675 #[test]
1676 fn ls_one_per_line() {
1677 let mut shell = shell();
1678 shell.exec("mkdir /test_dir").unwrap();
1679 shell.exec("touch /test_dir/aaa").unwrap();
1680 shell.exec("touch /test_dir/bbb").unwrap();
1681 let result = shell.exec("ls -1 /test_dir").unwrap();
1682 assert_eq!(result.stdout, "aaa\nbbb\n");
1683 }
1684
1685 #[test]
1686 fn ls_long_format() {
1687 let mut shell = shell();
1688 shell.exec("touch /myfile").unwrap();
1689 let result = shell.exec("ls -l /").unwrap();
1690 assert!(result.stdout.contains("myfile"));
1691 assert!(result.stdout.contains("rw"));
1693 }
1694
1695 #[test]
1696 fn ls_nonexistent() {
1697 let mut shell = shell();
1698 let result = shell.exec("ls /no_such_dir").unwrap();
1699 assert_ne!(result.exit_code, 0);
1700 assert!(result.stderr.contains("cannot access"));
1701 }
1702
1703 #[test]
1704 fn pwd_command() {
1705 let mut shell = shell();
1706 let result = shell.exec("pwd").unwrap();
1707 assert_eq!(result.stdout, "/\n");
1708 }
1709
1710 #[test]
1711 fn pwd_after_cd() {
1712 let mut shell = shell();
1713 shell.exec("mkdir /mydir").unwrap();
1714 shell.exec("cd /mydir").unwrap();
1715 let result = shell.exec("pwd").unwrap();
1716 assert_eq!(result.stdout, "/mydir\n");
1717 }
1718
1719 #[test]
1720 fn case_basic() {
1721 let mut shell = shell();
1722 let result = shell
1723 .exec("case hello in hello) echo matched;; world) echo nope;; esac")
1724 .unwrap();
1725 assert_eq!(result.stdout, "matched\n");
1726 }
1727
1728 #[test]
1729 fn case_wildcard() {
1730 let mut shell = shell();
1731 let result = shell
1732 .exec("case foo in bar) echo bar;; *) echo default;; esac")
1733 .unwrap();
1734 assert_eq!(result.stdout, "default\n");
1735 }
1736
1737 #[test]
1738 fn case_no_match() {
1739 let mut shell = shell();
1740 let result = shell.exec("case xyz in abc) echo nope;; esac").unwrap();
1741 assert_eq!(result.stdout, "");
1742 assert_eq!(result.exit_code, 0);
1743 }
1744
1745 #[test]
1746 fn register_default_commands_includes_new() {
1747 let cmds = crate::commands::register_default_commands();
1748 assert!(cmds.contains_key("touch"));
1749 assert!(cmds.contains_key("mkdir"));
1750 assert!(cmds.contains_key("ls"));
1751 assert!(cmds.contains_key("pwd"));
1752 }
1753
1754 #[test]
1757 fn complete_simple_commands() {
1758 assert!(RustBash::is_input_complete("echo hello"));
1759 assert!(RustBash::is_input_complete(""));
1760 assert!(RustBash::is_input_complete(" "));
1761 }
1762
1763 #[test]
1764 fn incomplete_unterminated_quotes() {
1765 assert!(!RustBash::is_input_complete("echo \"hello"));
1766 assert!(!RustBash::is_input_complete("echo 'hello"));
1767 }
1768
1769 #[test]
1770 fn incomplete_open_block() {
1771 assert!(!RustBash::is_input_complete("if true; then"));
1772 assert!(!RustBash::is_input_complete("for i in 1 2; do"));
1773 }
1774
1775 #[test]
1776 fn incomplete_trailing_pipe() {
1777 assert!(!RustBash::is_input_complete("echo hello |"));
1778 }
1779
1780 #[test]
1783 fn cwd_accessor() {
1784 let sh = shell();
1785 assert_eq!(sh.cwd(), "/");
1786 }
1787
1788 #[test]
1789 fn last_exit_code_accessor() {
1790 let mut sh = shell();
1791 sh.exec("false").unwrap();
1792 assert_eq!(sh.last_exit_code(), 1);
1793 }
1794
1795 #[test]
1796 fn command_names_accessor() {
1797 let sh = shell();
1798 let names = sh.command_names();
1799 assert!(names.contains(&"echo"));
1800 assert!(names.contains(&"cat"));
1801 }
1802
1803 #[test]
1804 fn builder_accepts_custom_fs() {
1805 let custom_fs = Arc::new(crate::vfs::InMemoryFs::new());
1806 custom_fs
1807 .write_file(std::path::Path::new("/pre-existing.txt"), b"hello")
1808 .unwrap();
1809
1810 let mut shell = RustBashBuilder::new().fs(custom_fs).build().unwrap();
1811
1812 let result = shell.exec("cat /pre-existing.txt").unwrap();
1813 assert_eq!(result.stdout.trim(), "hello");
1814 }
1815
1816 #[test]
1817 fn should_exit_accessor() {
1818 let mut sh = shell();
1819 assert!(!sh.should_exit());
1820 sh.exec("exit").unwrap();
1821 assert!(sh.should_exit());
1822 }
1823}