1#![warn(clippy::unwrap_used)]
364
365mod builtins;
366mod error;
367mod fs;
368mod git;
369mod interpreter;
370mod limits;
371#[cfg(feature = "logging")]
372mod logging_impl;
373mod network;
374pub mod parser;
376#[cfg(feature = "scripted_tool")]
379pub mod scripted_tool;
380pub mod tool;
382
383pub use async_trait::async_trait;
384pub use builtins::{Builtin, Context as BuiltinContext};
385pub use error::{Error, Result};
386pub use fs::{
387 verify_filesystem_requirements, DirEntry, FileSystem, FileType, FsBackend, FsLimitExceeded,
388 FsLimits, FsUsage, InMemoryFs, Metadata, MountableFs, OverlayFs, PosixFs, VfsSnapshot,
389};
390pub use git::GitConfig;
391pub use interpreter::{ControlFlow, ExecResult, OutputCallback, ShellState};
392pub use limits::{ExecutionCounters, ExecutionLimits, LimitExceeded};
393pub use network::NetworkAllowlist;
394pub use tool::{BashTool, BashToolBuilder, Tool, ToolRequest, ToolResponse, ToolStatus, VERSION};
395
396#[cfg(feature = "scripted_tool")]
397pub use scripted_tool::{ScriptedTool, ScriptedToolBuilder, ToolArgs, ToolCallback, ToolDef};
398
399#[cfg(feature = "http_client")]
400pub use network::HttpClient;
401
402#[cfg(feature = "git")]
403pub use git::GitClient;
404
405#[cfg(feature = "python")]
406pub use builtins::{PythonExternalFnHandler, PythonExternalFns, PythonLimits};
407#[cfg(feature = "python")]
411pub use monty::{ExcType, ExternalResult, MontyException, MontyObject};
412
413#[cfg(feature = "logging")]
418pub mod logging {
419 pub use crate::logging_impl::{format_script_for_log, sanitize_for_log, LogConfig};
420}
421
422#[cfg(feature = "logging")]
423pub use logging::LogConfig;
424
425use interpreter::Interpreter;
426use parser::Parser;
427use std::collections::HashMap;
428use std::path::PathBuf;
429use std::sync::Arc;
430
431pub struct Bash {
435 fs: Arc<dyn FileSystem>,
436 interpreter: Interpreter,
437 parser_timeout: std::time::Duration,
439 max_input_bytes: usize,
441 max_ast_depth: usize,
443 max_parser_operations: usize,
445 #[cfg(feature = "logging")]
447 log_config: logging::LogConfig,
448}
449
450impl Default for Bash {
451 fn default() -> Self {
452 Self::new()
453 }
454}
455
456impl Bash {
457 pub fn new() -> Self {
459 let fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
460 let interpreter = Interpreter::new(Arc::clone(&fs));
461 let parser_timeout = ExecutionLimits::default().parser_timeout;
462 let max_input_bytes = ExecutionLimits::default().max_input_bytes;
463 let max_ast_depth = ExecutionLimits::default().max_ast_depth;
464 let max_parser_operations = ExecutionLimits::default().max_parser_operations;
465 Self {
466 fs,
467 interpreter,
468 parser_timeout,
469 max_input_bytes,
470 max_ast_depth,
471 max_parser_operations,
472 #[cfg(feature = "logging")]
473 log_config: logging::LogConfig::default(),
474 }
475 }
476
477 pub fn builder() -> BashBuilder {
479 BashBuilder::default()
480 }
481
482 pub async fn exec(&mut self, script: &str) -> Result<ExecResult> {
488 #[cfg(feature = "logging")]
491 {
492 let script_info = logging::format_script_for_log(script, &self.log_config);
493 tracing::info!(target: "bashkit::session", script = %script_info, "Starting script execution");
494 }
495
496 let input_len = script.len();
498 if input_len > self.max_input_bytes {
499 #[cfg(feature = "logging")]
500 tracing::error!(
501 target: "bashkit::session",
502 input_len = input_len,
503 max_bytes = self.max_input_bytes,
504 "Script exceeds maximum input size"
505 );
506 return Err(Error::ResourceLimit(LimitExceeded::InputTooLarge(
507 input_len,
508 self.max_input_bytes,
509 )));
510 }
511
512 let parser_timeout = self.parser_timeout;
513 let max_ast_depth = self.max_ast_depth;
514 let max_parser_operations = self.max_parser_operations;
515 let script_owned = script.to_owned();
516
517 #[cfg(feature = "logging")]
518 tracing::debug!(
519 target: "bashkit::parser",
520 input_len = input_len,
521 max_ast_depth = max_ast_depth,
522 max_operations = max_parser_operations,
523 "Parsing script"
524 );
525
526 let parse_result = tokio::time::timeout(parser_timeout, async {
528 tokio::task::spawn_blocking(move || {
529 let parser =
530 Parser::with_limits(&script_owned, max_ast_depth, max_parser_operations);
531 parser.parse()
532 })
533 .await
534 })
535 .await;
536
537 let ast = match parse_result {
538 Ok(Ok(result)) => {
539 match &result {
540 Ok(_) => {
541 #[cfg(feature = "logging")]
542 tracing::debug!(target: "bashkit::parser", "Parse completed successfully");
543 }
544 Err(_e) => {
545 #[cfg(feature = "logging")]
546 tracing::warn!(target: "bashkit::parser", error = %_e, "Parse error");
547 }
548 }
549 result?
550 }
551 Ok(Err(join_error)) => {
552 #[cfg(feature = "logging")]
553 tracing::error!(
554 target: "bashkit::parser",
555 error = %join_error,
556 "Parser task failed"
557 );
558 return Err(Error::Parse(format!("parser task failed: {}", join_error)));
559 }
560 Err(_elapsed) => {
561 #[cfg(feature = "logging")]
562 tracing::error!(
563 target: "bashkit::parser",
564 timeout_ms = parser_timeout.as_millis() as u64,
565 "Parser timeout exceeded"
566 );
567 return Err(Error::ResourceLimit(LimitExceeded::ParserTimeout(
568 parser_timeout,
569 )));
570 }
571 };
572
573 #[cfg(feature = "logging")]
574 tracing::debug!(target: "bashkit::interpreter", "Starting interpretation");
575
576 let result = self.interpreter.execute(&ast).await;
577
578 #[cfg(feature = "logging")]
579 match &result {
580 Ok(exec_result) => {
581 tracing::info!(
582 target: "bashkit::session",
583 exit_code = exec_result.exit_code,
584 stdout_len = exec_result.stdout.len(),
585 stderr_len = exec_result.stderr.len(),
586 "Script execution completed"
587 );
588 }
589 Err(e) => {
590 tracing::error!(
591 target: "bashkit::session",
592 error = %e,
593 "Script execution failed"
594 );
595 }
596 }
597
598 result
599 }
600
601 pub async fn exec_streaming(
632 &mut self,
633 script: &str,
634 output_callback: OutputCallback,
635 ) -> Result<ExecResult> {
636 self.interpreter.set_output_callback(output_callback);
637 let result = self.exec(script).await;
638 self.interpreter.clear_output_callback();
639 result
640 }
641
642 pub fn fs(&self) -> Arc<dyn FileSystem> {
675 Arc::clone(&self.fs)
676 }
677
678 pub fn shell_state(&self) -> ShellState {
704 self.interpreter.shell_state()
705 }
706
707 pub fn restore_shell_state(&mut self, state: &ShellState) {
712 self.interpreter.restore_shell_state(state);
713 }
714}
715
716struct MountedFile {
753 path: PathBuf,
754 content: String,
755 mode: u32,
756}
757
758#[derive(Default)]
759pub struct BashBuilder {
760 fs: Option<Arc<dyn FileSystem>>,
761 env: HashMap<String, String>,
762 cwd: Option<PathBuf>,
763 limits: ExecutionLimits,
764 username: Option<String>,
765 hostname: Option<String>,
766 fixed_epoch: Option<i64>,
768 custom_builtins: HashMap<String, Box<dyn Builtin>>,
769 mounted_files: Vec<MountedFile>,
771 #[cfg(feature = "http_client")]
773 network_allowlist: Option<NetworkAllowlist>,
774 #[cfg(feature = "logging")]
776 log_config: Option<logging::LogConfig>,
777 #[cfg(feature = "git")]
779 git_config: Option<GitConfig>,
780}
781
782impl BashBuilder {
783 pub fn fs(mut self, fs: Arc<dyn FileSystem>) -> Self {
785 self.fs = Some(fs);
786 self
787 }
788
789 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
791 self.env.insert(key.into(), value.into());
792 self
793 }
794
795 pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
797 self.cwd = Some(cwd.into());
798 self
799 }
800
801 pub fn limits(mut self, limits: ExecutionLimits) -> Self {
803 self.limits = limits;
804 self
805 }
806
807 pub fn username(mut self, username: impl Into<String>) -> Self {
812 self.username = Some(username.into());
813 self
814 }
815
816 pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
820 self.hostname = Some(hostname.into());
821 self
822 }
823
824 pub fn fixed_epoch(mut self, epoch: i64) -> Self {
829 self.fixed_epoch = Some(epoch);
830 self
831 }
832
833 #[cfg(feature = "http_client")]
865 pub fn network(mut self, allowlist: NetworkAllowlist) -> Self {
866 self.network_allowlist = Some(allowlist);
867 self
868 }
869
870 #[cfg(feature = "logging")]
907 pub fn log_config(mut self, config: logging::LogConfig) -> Self {
908 self.log_config = Some(config);
909 self
910 }
911
912 #[cfg(feature = "git")]
941 pub fn git(mut self, config: GitConfig) -> Self {
942 self.git_config = Some(config);
943 self
944 }
945
946 #[cfg(feature = "python")]
961 pub fn python(self) -> Self {
962 self.python_with_limits(builtins::PythonLimits::default())
963 }
964
965 #[cfg(feature = "python")]
980 pub fn python_with_limits(self, limits: builtins::PythonLimits) -> Self {
981 self.builtin(
982 "python",
983 Box::new(builtins::Python::with_limits(limits.clone())),
984 )
985 .builtin("python3", Box::new(builtins::Python::with_limits(limits)))
986 }
987
988 #[cfg(feature = "python")]
992 pub fn python_with_external_handler(
993 self,
994 limits: builtins::PythonLimits,
995 external_fns: Vec<String>,
996 handler: builtins::PythonExternalFnHandler,
997 ) -> Self {
998 self.builtin(
999 "python",
1000 Box::new(
1001 builtins::Python::with_limits(limits.clone())
1002 .with_external_handler(external_fns.clone(), handler.clone()),
1003 ),
1004 )
1005 .builtin(
1006 "python3",
1007 Box::new(
1008 builtins::Python::with_limits(limits).with_external_handler(external_fns, handler),
1009 ),
1010 )
1011 }
1012
1013 pub fn builtin(mut self, name: impl Into<String>, builtin: Box<dyn Builtin>) -> Self {
1050 self.custom_builtins.insert(name.into(), builtin);
1051 self
1052 }
1053
1054 pub fn mount_text(mut self, path: impl Into<PathBuf>, content: impl Into<String>) -> Self {
1083 self.mounted_files.push(MountedFile {
1084 path: path.into(),
1085 content: content.into(),
1086 mode: 0o644,
1087 });
1088 self
1089 }
1090
1091 pub fn mount_readonly_text(
1130 mut self,
1131 path: impl Into<PathBuf>,
1132 content: impl Into<String>,
1133 ) -> Self {
1134 self.mounted_files.push(MountedFile {
1135 path: path.into(),
1136 content: content.into(),
1137 mode: 0o444,
1138 });
1139 self
1140 }
1141
1142 pub fn build(self) -> Bash {
1177 let base_fs = self.fs.unwrap_or_else(|| Arc::new(InMemoryFs::new()));
1178
1179 let fs: Arc<dyn FileSystem> = if self.mounted_files.is_empty() {
1181 base_fs
1182 } else {
1183 let overlay = OverlayFs::new(base_fs);
1184 for mf in &self.mounted_files {
1186 overlay.upper().add_file(&mf.path, &mf.content, mf.mode);
1187 }
1188 Arc::new(overlay)
1189 };
1190
1191 Self::build_with_fs(
1192 fs,
1193 self.env,
1194 self.username,
1195 self.hostname,
1196 self.fixed_epoch,
1197 self.cwd,
1198 self.limits,
1199 self.custom_builtins,
1200 #[cfg(feature = "http_client")]
1201 self.network_allowlist,
1202 #[cfg(feature = "logging")]
1203 self.log_config,
1204 #[cfg(feature = "git")]
1205 self.git_config,
1206 )
1207 }
1208
1209 #[allow(clippy::too_many_arguments)]
1211 fn build_with_fs(
1212 fs: Arc<dyn FileSystem>,
1213 env: HashMap<String, String>,
1214 username: Option<String>,
1215 hostname: Option<String>,
1216 fixed_epoch: Option<i64>,
1217 cwd: Option<PathBuf>,
1218 limits: ExecutionLimits,
1219 custom_builtins: HashMap<String, Box<dyn Builtin>>,
1220 #[cfg(feature = "http_client")] network_allowlist: Option<NetworkAllowlist>,
1221 #[cfg(feature = "logging")] log_config: Option<logging::LogConfig>,
1222 #[cfg(feature = "git")] git_config: Option<GitConfig>,
1223 ) -> Bash {
1224 #[cfg(feature = "logging")]
1225 let log_config = log_config.unwrap_or_default();
1226
1227 #[cfg(feature = "logging")]
1228 tracing::debug!(
1229 target: "bashkit::config",
1230 redact_sensitive = log_config.redact_sensitive,
1231 log_scripts = log_config.log_script_content,
1232 "Bash instance configured"
1233 );
1234
1235 let mut interpreter = Interpreter::with_config(
1236 Arc::clone(&fs),
1237 username.clone(),
1238 hostname,
1239 fixed_epoch,
1240 custom_builtins,
1241 );
1242
1243 for (key, value) in &env {
1245 interpreter.set_env(key, value);
1246 interpreter.set_var(key, value);
1249 }
1250 drop(env);
1251
1252 if let Some(ref username) = username {
1254 interpreter.set_env("USER", username);
1255 interpreter.set_var("USER", username);
1256 }
1257
1258 if let Some(cwd) = cwd {
1259 interpreter.set_cwd(cwd);
1260 }
1261
1262 #[cfg(feature = "http_client")]
1264 if let Some(allowlist) = network_allowlist {
1265 let client = network::HttpClient::new(allowlist);
1266 interpreter.set_http_client(client);
1267 }
1268
1269 #[cfg(feature = "git")]
1271 if let Some(config) = git_config {
1272 let client = git::GitClient::new(config);
1273 interpreter.set_git_client(client);
1274 }
1275
1276 let parser_timeout = limits.parser_timeout;
1277 let max_input_bytes = limits.max_input_bytes;
1278 let max_ast_depth = limits.max_ast_depth;
1279 let max_parser_operations = limits.max_parser_operations;
1280 interpreter.set_limits(limits);
1281
1282 Bash {
1283 fs,
1284 interpreter,
1285 parser_timeout,
1286 max_input_bytes,
1287 max_ast_depth,
1288 max_parser_operations,
1289 #[cfg(feature = "logging")]
1290 log_config,
1291 }
1292 }
1293}
1294
1295#[doc = include_str!("../docs/custom_builtins.md")]
1312pub mod custom_builtins_guide {}
1313
1314#[doc = include_str!("../docs/compatibility.md")]
1324pub mod compatibility_scorecard {}
1325
1326#[doc = include_str!("../docs/threat-model.md")]
1340pub mod threat_model {}
1341
1342#[cfg(feature = "python")]
1356#[doc = include_str!("../docs/python.md")]
1357pub mod python_guide {}
1358
1359#[cfg(feature = "logging")]
1372#[doc = include_str!("../docs/logging.md")]
1373pub mod logging_guide {}
1374
1375#[cfg(test)]
1376#[allow(clippy::unwrap_used)]
1377mod tests {
1378 use super::*;
1379 use std::sync::{Arc, Mutex};
1380
1381 #[tokio::test]
1382 async fn test_echo_hello() {
1383 let mut bash = Bash::new();
1384 let result = bash.exec("echo hello").await.unwrap();
1385 assert_eq!(result.stdout, "hello\n");
1386 assert_eq!(result.exit_code, 0);
1387 }
1388
1389 #[tokio::test]
1390 async fn test_echo_multiple_args() {
1391 let mut bash = Bash::new();
1392 let result = bash.exec("echo hello world").await.unwrap();
1393 assert_eq!(result.stdout, "hello world\n");
1394 assert_eq!(result.exit_code, 0);
1395 }
1396
1397 #[tokio::test]
1398 async fn test_variable_expansion() {
1399 let mut bash = Bash::builder().env("HOME", "/home/user").build();
1400 let result = bash.exec("echo $HOME").await.unwrap();
1401 assert_eq!(result.stdout, "/home/user\n");
1402 assert_eq!(result.exit_code, 0);
1403 }
1404
1405 #[tokio::test]
1406 async fn test_variable_brace_expansion() {
1407 let mut bash = Bash::builder().env("USER", "testuser").build();
1408 let result = bash.exec("echo ${USER}").await.unwrap();
1409 assert_eq!(result.stdout, "testuser\n");
1410 }
1411
1412 #[tokio::test]
1413 async fn test_undefined_variable_expands_to_empty() {
1414 let mut bash = Bash::new();
1415 let result = bash.exec("echo $UNDEFINED_VAR").await.unwrap();
1416 assert_eq!(result.stdout, "\n");
1417 }
1418
1419 #[tokio::test]
1420 async fn test_pipeline() {
1421 let mut bash = Bash::new();
1422 let result = bash.exec("echo hello | cat").await.unwrap();
1423 assert_eq!(result.stdout, "hello\n");
1424 }
1425
1426 #[tokio::test]
1427 async fn test_pipeline_three_commands() {
1428 let mut bash = Bash::new();
1429 let result = bash.exec("echo hello | cat | cat").await.unwrap();
1430 assert_eq!(result.stdout, "hello\n");
1431 }
1432
1433 #[tokio::test]
1434 async fn test_redirect_output() {
1435 let mut bash = Bash::new();
1436 let result = bash.exec("echo hello > /tmp/test.txt").await.unwrap();
1437 assert_eq!(result.stdout, "");
1438 assert_eq!(result.exit_code, 0);
1439
1440 let result = bash.exec("cat /tmp/test.txt").await.unwrap();
1442 assert_eq!(result.stdout, "hello\n");
1443 }
1444
1445 #[tokio::test]
1446 async fn test_redirect_append() {
1447 let mut bash = Bash::new();
1448 bash.exec("echo hello > /tmp/append.txt").await.unwrap();
1449 bash.exec("echo world >> /tmp/append.txt").await.unwrap();
1450
1451 let result = bash.exec("cat /tmp/append.txt").await.unwrap();
1452 assert_eq!(result.stdout, "hello\nworld\n");
1453 }
1454
1455 #[tokio::test]
1456 async fn test_command_list_and() {
1457 let mut bash = Bash::new();
1458 let result = bash.exec("true && echo success").await.unwrap();
1459 assert_eq!(result.stdout, "success\n");
1460 }
1461
1462 #[tokio::test]
1463 async fn test_command_list_and_short_circuit() {
1464 let mut bash = Bash::new();
1465 let result = bash.exec("false && echo should_not_print").await.unwrap();
1466 assert_eq!(result.stdout, "");
1467 assert_eq!(result.exit_code, 1);
1468 }
1469
1470 #[tokio::test]
1471 async fn test_command_list_or() {
1472 let mut bash = Bash::new();
1473 let result = bash.exec("false || echo fallback").await.unwrap();
1474 assert_eq!(result.stdout, "fallback\n");
1475 }
1476
1477 #[tokio::test]
1478 async fn test_command_list_or_short_circuit() {
1479 let mut bash = Bash::new();
1480 let result = bash.exec("true || echo should_not_print").await.unwrap();
1481 assert_eq!(result.stdout, "");
1482 assert_eq!(result.exit_code, 0);
1483 }
1484
1485 #[tokio::test]
1487 async fn test_phase1_target() {
1488 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
1489
1490 let result = bash
1491 .exec("echo $HOME | cat > /tmp/out && cat /tmp/out")
1492 .await
1493 .unwrap();
1494
1495 assert_eq!(result.stdout, "/home/testuser\n");
1496 assert_eq!(result.exit_code, 0);
1497 }
1498
1499 #[tokio::test]
1500 async fn test_redirect_input() {
1501 let mut bash = Bash::new();
1502 bash.exec("echo hello > /tmp/input.txt").await.unwrap();
1504
1505 let result = bash.exec("cat < /tmp/input.txt").await.unwrap();
1507 assert_eq!(result.stdout, "hello\n");
1508 }
1509
1510 #[tokio::test]
1511 async fn test_here_string() {
1512 let mut bash = Bash::new();
1513 let result = bash.exec("cat <<< hello").await.unwrap();
1514 assert_eq!(result.stdout, "hello\n");
1515 }
1516
1517 #[tokio::test]
1518 async fn test_if_true() {
1519 let mut bash = Bash::new();
1520 let result = bash.exec("if true; then echo yes; fi").await.unwrap();
1521 assert_eq!(result.stdout, "yes\n");
1522 }
1523
1524 #[tokio::test]
1525 async fn test_if_false() {
1526 let mut bash = Bash::new();
1527 let result = bash.exec("if false; then echo yes; fi").await.unwrap();
1528 assert_eq!(result.stdout, "");
1529 }
1530
1531 #[tokio::test]
1532 async fn test_if_else() {
1533 let mut bash = Bash::new();
1534 let result = bash
1535 .exec("if false; then echo yes; else echo no; fi")
1536 .await
1537 .unwrap();
1538 assert_eq!(result.stdout, "no\n");
1539 }
1540
1541 #[tokio::test]
1542 async fn test_if_elif() {
1543 let mut bash = Bash::new();
1544 let result = bash
1545 .exec("if false; then echo one; elif true; then echo two; else echo three; fi")
1546 .await
1547 .unwrap();
1548 assert_eq!(result.stdout, "two\n");
1549 }
1550
1551 #[tokio::test]
1552 async fn test_for_loop() {
1553 let mut bash = Bash::new();
1554 let result = bash.exec("for i in a b c; do echo $i; done").await.unwrap();
1555 assert_eq!(result.stdout, "a\nb\nc\n");
1556 }
1557
1558 #[tokio::test]
1559 async fn test_for_loop_positional_params() {
1560 let mut bash = Bash::new();
1561 let result = bash
1563 .exec("f() { for x; do echo $x; done; }; f one two three")
1564 .await
1565 .unwrap();
1566 assert_eq!(result.stdout, "one\ntwo\nthree\n");
1567 }
1568
1569 #[tokio::test]
1570 async fn test_while_loop() {
1571 let mut bash = Bash::new();
1572 let result = bash.exec("while false; do echo loop; done").await.unwrap();
1574 assert_eq!(result.stdout, "");
1575 }
1576
1577 #[tokio::test]
1578 async fn test_subshell() {
1579 let mut bash = Bash::new();
1580 let result = bash.exec("(echo hello)").await.unwrap();
1581 assert_eq!(result.stdout, "hello\n");
1582 }
1583
1584 #[tokio::test]
1585 async fn test_brace_group() {
1586 let mut bash = Bash::new();
1587 let result = bash.exec("{ echo hello; }").await.unwrap();
1588 assert_eq!(result.stdout, "hello\n");
1589 }
1590
1591 #[tokio::test]
1592 async fn test_function_keyword() {
1593 let mut bash = Bash::new();
1594 let result = bash
1595 .exec("function greet { echo hello; }; greet")
1596 .await
1597 .unwrap();
1598 assert_eq!(result.stdout, "hello\n");
1599 }
1600
1601 #[tokio::test]
1602 async fn test_function_posix() {
1603 let mut bash = Bash::new();
1604 let result = bash.exec("greet() { echo hello; }; greet").await.unwrap();
1605 assert_eq!(result.stdout, "hello\n");
1606 }
1607
1608 #[tokio::test]
1609 async fn test_function_args() {
1610 let mut bash = Bash::new();
1611 let result = bash
1612 .exec("greet() { echo $1 $2; }; greet world foo")
1613 .await
1614 .unwrap();
1615 assert_eq!(result.stdout, "world foo\n");
1616 }
1617
1618 #[tokio::test]
1619 async fn test_function_arg_count() {
1620 let mut bash = Bash::new();
1621 let result = bash
1622 .exec("count() { echo $#; }; count a b c")
1623 .await
1624 .unwrap();
1625 assert_eq!(result.stdout, "3\n");
1626 }
1627
1628 #[tokio::test]
1629 async fn test_case_literal() {
1630 let mut bash = Bash::new();
1631 let result = bash
1632 .exec("case foo in foo) echo matched ;; esac")
1633 .await
1634 .unwrap();
1635 assert_eq!(result.stdout, "matched\n");
1636 }
1637
1638 #[tokio::test]
1639 async fn test_case_wildcard() {
1640 let mut bash = Bash::new();
1641 let result = bash
1642 .exec("case bar in *) echo default ;; esac")
1643 .await
1644 .unwrap();
1645 assert_eq!(result.stdout, "default\n");
1646 }
1647
1648 #[tokio::test]
1649 async fn test_case_no_match() {
1650 let mut bash = Bash::new();
1651 let result = bash.exec("case foo in bar) echo no ;; esac").await.unwrap();
1652 assert_eq!(result.stdout, "");
1653 }
1654
1655 #[tokio::test]
1656 async fn test_case_multiple_patterns() {
1657 let mut bash = Bash::new();
1658 let result = bash
1659 .exec("case foo in bar|foo|baz) echo matched ;; esac")
1660 .await
1661 .unwrap();
1662 assert_eq!(result.stdout, "matched\n");
1663 }
1664
1665 #[tokio::test]
1666 async fn test_case_bracket_expr() {
1667 let mut bash = Bash::new();
1668 let result = bash
1670 .exec("case b in [abc]) echo matched ;; esac")
1671 .await
1672 .unwrap();
1673 assert_eq!(result.stdout, "matched\n");
1674 }
1675
1676 #[tokio::test]
1677 async fn test_case_bracket_range() {
1678 let mut bash = Bash::new();
1679 let result = bash
1681 .exec("case m in [a-z]) echo letter ;; esac")
1682 .await
1683 .unwrap();
1684 assert_eq!(result.stdout, "letter\n");
1685 }
1686
1687 #[tokio::test]
1688 async fn test_case_bracket_negation() {
1689 let mut bash = Bash::new();
1690 let result = bash
1692 .exec("case x in [!abc]) echo not_abc ;; esac")
1693 .await
1694 .unwrap();
1695 assert_eq!(result.stdout, "not_abc\n");
1696 }
1697
1698 #[tokio::test]
1699 async fn test_break_as_command() {
1700 let mut bash = Bash::new();
1701 let result = bash.exec("break").await.unwrap();
1703 assert_eq!(result.exit_code, 0);
1705 }
1706
1707 #[tokio::test]
1708 async fn test_for_one_item() {
1709 let mut bash = Bash::new();
1710 let result = bash.exec("for i in a; do echo $i; done").await.unwrap();
1712 assert_eq!(result.stdout, "a\n");
1713 }
1714
1715 #[tokio::test]
1716 async fn test_for_with_break() {
1717 let mut bash = Bash::new();
1718 let result = bash.exec("for i in a; do break; done").await.unwrap();
1720 assert_eq!(result.stdout, "");
1721 assert_eq!(result.exit_code, 0);
1722 }
1723
1724 #[tokio::test]
1725 async fn test_for_echo_break() {
1726 let mut bash = Bash::new();
1727 let result = bash
1729 .exec("for i in a b c; do echo $i; break; done")
1730 .await
1731 .unwrap();
1732 assert_eq!(result.stdout, "a\n");
1733 }
1734
1735 #[tokio::test]
1736 async fn test_test_string_empty() {
1737 let mut bash = Bash::new();
1738 let result = bash.exec("test -z '' && echo yes").await.unwrap();
1739 assert_eq!(result.stdout, "yes\n");
1740 }
1741
1742 #[tokio::test]
1743 async fn test_test_string_not_empty() {
1744 let mut bash = Bash::new();
1745 let result = bash.exec("test -n 'hello' && echo yes").await.unwrap();
1746 assert_eq!(result.stdout, "yes\n");
1747 }
1748
1749 #[tokio::test]
1750 async fn test_test_string_equal() {
1751 let mut bash = Bash::new();
1752 let result = bash.exec("test foo = foo && echo yes").await.unwrap();
1753 assert_eq!(result.stdout, "yes\n");
1754 }
1755
1756 #[tokio::test]
1757 async fn test_test_string_not_equal() {
1758 let mut bash = Bash::new();
1759 let result = bash.exec("test foo != bar && echo yes").await.unwrap();
1760 assert_eq!(result.stdout, "yes\n");
1761 }
1762
1763 #[tokio::test]
1764 async fn test_test_numeric_equal() {
1765 let mut bash = Bash::new();
1766 let result = bash.exec("test 5 -eq 5 && echo yes").await.unwrap();
1767 assert_eq!(result.stdout, "yes\n");
1768 }
1769
1770 #[tokio::test]
1771 async fn test_test_numeric_less_than() {
1772 let mut bash = Bash::new();
1773 let result = bash.exec("test 3 -lt 5 && echo yes").await.unwrap();
1774 assert_eq!(result.stdout, "yes\n");
1775 }
1776
1777 #[tokio::test]
1778 async fn test_bracket_form() {
1779 let mut bash = Bash::new();
1780 let result = bash.exec("[ foo = foo ] && echo yes").await.unwrap();
1781 assert_eq!(result.stdout, "yes\n");
1782 }
1783
1784 #[tokio::test]
1785 async fn test_if_with_test() {
1786 let mut bash = Bash::new();
1787 let result = bash
1788 .exec("if [ 5 -gt 3 ]; then echo bigger; fi")
1789 .await
1790 .unwrap();
1791 assert_eq!(result.stdout, "bigger\n");
1792 }
1793
1794 #[tokio::test]
1795 async fn test_variable_assignment() {
1796 let mut bash = Bash::new();
1797 let result = bash.exec("FOO=bar; echo $FOO").await.unwrap();
1798 assert_eq!(result.stdout, "bar\n");
1799 }
1800
1801 #[tokio::test]
1802 async fn test_variable_assignment_inline() {
1803 let mut bash = Bash::new();
1804 let result = bash.exec("MSG=hello; echo $MSG world").await.unwrap();
1806 assert_eq!(result.stdout, "hello world\n");
1807 }
1808
1809 #[tokio::test]
1810 async fn test_variable_assignment_only() {
1811 let mut bash = Bash::new();
1812 let result = bash.exec("FOO=bar").await.unwrap();
1814 assert_eq!(result.stdout, "");
1815 assert_eq!(result.exit_code, 0);
1816
1817 let result = bash.exec("echo $FOO").await.unwrap();
1819 assert_eq!(result.stdout, "bar\n");
1820 }
1821
1822 #[tokio::test]
1823 async fn test_multiple_assignments() {
1824 let mut bash = Bash::new();
1825 let result = bash.exec("A=1; B=2; C=3; echo $A $B $C").await.unwrap();
1826 assert_eq!(result.stdout, "1 2 3\n");
1827 }
1828
1829 #[tokio::test]
1830 async fn test_prefix_assignment_visible_in_env() {
1831 let mut bash = Bash::new();
1832 let result = bash.exec("MYVAR=hello printenv MYVAR").await.unwrap();
1834 assert_eq!(result.stdout, "hello\n");
1835 }
1836
1837 #[tokio::test]
1838 async fn test_prefix_assignment_temporary() {
1839 let mut bash = Bash::new();
1840 bash.exec("MYVAR=hello printenv MYVAR").await.unwrap();
1842 let result = bash.exec("echo ${MYVAR:-unset}").await.unwrap();
1843 assert_eq!(result.stdout, "unset\n");
1844 }
1845
1846 #[tokio::test]
1847 async fn test_prefix_assignment_does_not_clobber_existing_env() {
1848 let mut bash = Bash::new();
1849 let result = bash
1851 .exec("EXISTING=original; export EXISTING; EXISTING=temp printenv EXISTING")
1852 .await
1853 .unwrap();
1854 assert_eq!(result.stdout, "temp\n");
1855 }
1856
1857 #[tokio::test]
1858 async fn test_prefix_assignment_multiple_vars() {
1859 let mut bash = Bash::new();
1860 let result = bash.exec("A=one B=two printenv A").await.unwrap();
1862 assert_eq!(result.stdout, "one\n");
1863 assert_eq!(result.exit_code, 0);
1864 }
1865
1866 #[tokio::test]
1867 async fn test_prefix_assignment_empty_value() {
1868 let mut bash = Bash::new();
1869 let result = bash.exec("MYVAR= printenv MYVAR").await.unwrap();
1871 assert_eq!(result.stdout, "\n");
1872 assert_eq!(result.exit_code, 0);
1873 }
1874
1875 #[tokio::test]
1876 async fn test_prefix_assignment_not_found_without_prefix() {
1877 let mut bash = Bash::new();
1878 let result = bash.exec("printenv NONEXISTENT").await.unwrap();
1880 assert_eq!(result.stdout, "");
1881 assert_eq!(result.exit_code, 1);
1882 }
1883
1884 #[tokio::test]
1885 async fn test_prefix_assignment_does_not_persist_in_variables() {
1886 let mut bash = Bash::new();
1887 bash.exec("TMPVAR=gone echo ok").await.unwrap();
1889 let result = bash.exec("echo \"${TMPVAR:-unset}\"").await.unwrap();
1890 assert_eq!(result.stdout, "unset\n");
1891 }
1892
1893 #[tokio::test]
1894 async fn test_assignment_only_persists() {
1895 let mut bash = Bash::new();
1896 bash.exec("PERSIST=yes").await.unwrap();
1898 let result = bash.exec("echo $PERSIST").await.unwrap();
1899 assert_eq!(result.stdout, "yes\n");
1900 }
1901
1902 #[tokio::test]
1903 async fn test_printf_string() {
1904 let mut bash = Bash::new();
1905 let result = bash.exec("printf '%s' hello").await.unwrap();
1906 assert_eq!(result.stdout, "hello");
1907 }
1908
1909 #[tokio::test]
1910 async fn test_printf_newline() {
1911 let mut bash = Bash::new();
1912 let result = bash.exec("printf 'hello\\n'").await.unwrap();
1913 assert_eq!(result.stdout, "hello\n");
1914 }
1915
1916 #[tokio::test]
1917 async fn test_printf_multiple_args() {
1918 let mut bash = Bash::new();
1919 let result = bash.exec("printf '%s %s\\n' hello world").await.unwrap();
1920 assert_eq!(result.stdout, "hello world\n");
1921 }
1922
1923 #[tokio::test]
1924 async fn test_printf_integer() {
1925 let mut bash = Bash::new();
1926 let result = bash.exec("printf '%d' 42").await.unwrap();
1927 assert_eq!(result.stdout, "42");
1928 }
1929
1930 #[tokio::test]
1931 async fn test_export() {
1932 let mut bash = Bash::new();
1933 let result = bash.exec("export FOO=bar; echo $FOO").await.unwrap();
1934 assert_eq!(result.stdout, "bar\n");
1935 }
1936
1937 #[tokio::test]
1938 async fn test_read_basic() {
1939 let mut bash = Bash::new();
1940 let result = bash.exec("echo hello | read VAR; echo $VAR").await.unwrap();
1941 assert_eq!(result.stdout, "hello\n");
1942 }
1943
1944 #[tokio::test]
1945 async fn test_read_multiple_vars() {
1946 let mut bash = Bash::new();
1947 let result = bash
1948 .exec("echo 'a b c' | read X Y Z; echo $X $Y $Z")
1949 .await
1950 .unwrap();
1951 assert_eq!(result.stdout, "a b c\n");
1952 }
1953
1954 #[tokio::test]
1955 async fn test_glob_star() {
1956 let mut bash = Bash::new();
1957 bash.exec("echo a > /tmp/file1.txt").await.unwrap();
1959 bash.exec("echo b > /tmp/file2.txt").await.unwrap();
1960 bash.exec("echo c > /tmp/other.log").await.unwrap();
1961
1962 let result = bash.exec("echo /tmp/*.txt").await.unwrap();
1964 assert_eq!(result.stdout, "/tmp/file1.txt /tmp/file2.txt\n");
1965 }
1966
1967 #[tokio::test]
1968 async fn test_glob_question_mark() {
1969 let mut bash = Bash::new();
1970 bash.exec("echo a > /tmp/a1.txt").await.unwrap();
1972 bash.exec("echo b > /tmp/a2.txt").await.unwrap();
1973 bash.exec("echo c > /tmp/a10.txt").await.unwrap();
1974
1975 let result = bash.exec("echo /tmp/a?.txt").await.unwrap();
1977 assert_eq!(result.stdout, "/tmp/a1.txt /tmp/a2.txt\n");
1978 }
1979
1980 #[tokio::test]
1981 async fn test_glob_no_match() {
1982 let mut bash = Bash::new();
1983 let result = bash.exec("echo /nonexistent/*.xyz").await.unwrap();
1985 assert_eq!(result.stdout, "/nonexistent/*.xyz\n");
1986 }
1987
1988 #[tokio::test]
1989 async fn test_command_substitution() {
1990 let mut bash = Bash::new();
1991 let result = bash.exec("echo $(echo hello)").await.unwrap();
1992 assert_eq!(result.stdout, "hello\n");
1993 }
1994
1995 #[tokio::test]
1996 async fn test_command_substitution_in_string() {
1997 let mut bash = Bash::new();
1998 let result = bash.exec("echo \"result: $(echo 42)\"").await.unwrap();
1999 assert_eq!(result.stdout, "result: 42\n");
2000 }
2001
2002 #[tokio::test]
2003 async fn test_command_substitution_pipeline() {
2004 let mut bash = Bash::new();
2005 let result = bash.exec("echo $(echo hello | cat)").await.unwrap();
2006 assert_eq!(result.stdout, "hello\n");
2007 }
2008
2009 #[tokio::test]
2010 async fn test_command_substitution_variable() {
2011 let mut bash = Bash::new();
2012 let result = bash.exec("VAR=$(echo test); echo $VAR").await.unwrap();
2013 assert_eq!(result.stdout, "test\n");
2014 }
2015
2016 #[tokio::test]
2017 async fn test_arithmetic_simple() {
2018 let mut bash = Bash::new();
2019 let result = bash.exec("echo $((1 + 2))").await.unwrap();
2020 assert_eq!(result.stdout, "3\n");
2021 }
2022
2023 #[tokio::test]
2024 async fn test_arithmetic_multiply() {
2025 let mut bash = Bash::new();
2026 let result = bash.exec("echo $((3 * 4))").await.unwrap();
2027 assert_eq!(result.stdout, "12\n");
2028 }
2029
2030 #[tokio::test]
2031 async fn test_arithmetic_with_variable() {
2032 let mut bash = Bash::new();
2033 let result = bash.exec("X=5; echo $((X + 3))").await.unwrap();
2034 assert_eq!(result.stdout, "8\n");
2035 }
2036
2037 #[tokio::test]
2038 async fn test_arithmetic_complex() {
2039 let mut bash = Bash::new();
2040 let result = bash.exec("echo $((2 + 3 * 4))").await.unwrap();
2041 assert_eq!(result.stdout, "14\n");
2042 }
2043
2044 #[tokio::test]
2045 async fn test_heredoc_simple() {
2046 let mut bash = Bash::new();
2047 let result = bash.exec("cat <<EOF\nhello\nworld\nEOF").await.unwrap();
2048 assert_eq!(result.stdout, "hello\nworld\n");
2049 }
2050
2051 #[tokio::test]
2052 async fn test_heredoc_single_line() {
2053 let mut bash = Bash::new();
2054 let result = bash.exec("cat <<END\ntest\nEND").await.unwrap();
2055 assert_eq!(result.stdout, "test\n");
2056 }
2057
2058 #[tokio::test]
2059 async fn test_unset() {
2060 let mut bash = Bash::new();
2061 let result = bash
2062 .exec("FOO=bar; unset FOO; echo \"x${FOO}y\"")
2063 .await
2064 .unwrap();
2065 assert_eq!(result.stdout, "xy\n");
2066 }
2067
2068 #[tokio::test]
2069 async fn test_local_basic() {
2070 let mut bash = Bash::new();
2071 let result = bash.exec("local X=test; echo $X").await.unwrap();
2073 assert_eq!(result.stdout, "test\n");
2074 }
2075
2076 #[tokio::test]
2077 async fn test_set_option() {
2078 let mut bash = Bash::new();
2079 let result = bash.exec("set -e; echo ok").await.unwrap();
2080 assert_eq!(result.stdout, "ok\n");
2081 }
2082
2083 #[tokio::test]
2084 async fn test_param_default() {
2085 let mut bash = Bash::new();
2086 let result = bash.exec("echo ${UNSET:-default}").await.unwrap();
2088 assert_eq!(result.stdout, "default\n");
2089
2090 let result = bash.exec("X=value; echo ${X:-default}").await.unwrap();
2092 assert_eq!(result.stdout, "value\n");
2093 }
2094
2095 #[tokio::test]
2096 async fn test_param_assign_default() {
2097 let mut bash = Bash::new();
2098 let result = bash.exec("echo ${NEW:=assigned}; echo $NEW").await.unwrap();
2100 assert_eq!(result.stdout, "assigned\nassigned\n");
2101 }
2102
2103 #[tokio::test]
2104 async fn test_param_length() {
2105 let mut bash = Bash::new();
2106 let result = bash.exec("X=hello; echo ${#X}").await.unwrap();
2107 assert_eq!(result.stdout, "5\n");
2108 }
2109
2110 #[tokio::test]
2111 async fn test_param_remove_prefix() {
2112 let mut bash = Bash::new();
2113 let result = bash.exec("X=hello.world.txt; echo ${X#*.}").await.unwrap();
2115 assert_eq!(result.stdout, "world.txt\n");
2116 }
2117
2118 #[tokio::test]
2119 async fn test_param_remove_suffix() {
2120 let mut bash = Bash::new();
2121 let result = bash.exec("X=file.tar.gz; echo ${X%.*}").await.unwrap();
2123 assert_eq!(result.stdout, "file.tar\n");
2124 }
2125
2126 #[tokio::test]
2127 async fn test_array_basic() {
2128 let mut bash = Bash::new();
2129 let result = bash.exec("arr=(a b c); echo ${arr[1]}").await.unwrap();
2131 assert_eq!(result.stdout, "b\n");
2132 }
2133
2134 #[tokio::test]
2135 async fn test_array_all_elements() {
2136 let mut bash = Bash::new();
2137 let result = bash
2139 .exec("arr=(one two three); echo ${arr[@]}")
2140 .await
2141 .unwrap();
2142 assert_eq!(result.stdout, "one two three\n");
2143 }
2144
2145 #[tokio::test]
2146 async fn test_array_length() {
2147 let mut bash = Bash::new();
2148 let result = bash.exec("arr=(a b c d e); echo ${#arr[@]}").await.unwrap();
2150 assert_eq!(result.stdout, "5\n");
2151 }
2152
2153 #[tokio::test]
2154 async fn test_array_indexed_assignment() {
2155 let mut bash = Bash::new();
2156 let result = bash
2158 .exec("arr[0]=first; arr[1]=second; echo ${arr[0]} ${arr[1]}")
2159 .await
2160 .unwrap();
2161 assert_eq!(result.stdout, "first second\n");
2162 }
2163
2164 #[tokio::test]
2167 async fn test_command_limit() {
2168 let limits = ExecutionLimits::new().max_commands(5);
2169 let mut bash = Bash::builder().limits(limits).build();
2170
2171 let result = bash.exec("true; true; true; true; true; true").await;
2173 assert!(result.is_err());
2174 let err = result.unwrap_err();
2175 assert!(
2176 err.to_string().contains("maximum command count exceeded"),
2177 "Expected command limit error, got: {}",
2178 err
2179 );
2180 }
2181
2182 #[tokio::test]
2183 async fn test_command_limit_not_exceeded() {
2184 let limits = ExecutionLimits::new().max_commands(10);
2185 let mut bash = Bash::builder().limits(limits).build();
2186
2187 let result = bash.exec("true; true; true; true; true").await.unwrap();
2189 assert_eq!(result.exit_code, 0);
2190 }
2191
2192 #[tokio::test]
2193 async fn test_loop_iteration_limit() {
2194 let limits = ExecutionLimits::new().max_loop_iterations(5);
2195 let mut bash = Bash::builder().limits(limits).build();
2196
2197 let result = bash
2199 .exec("for i in 1 2 3 4 5 6 7 8 9 10; do echo $i; done")
2200 .await;
2201 assert!(result.is_err());
2202 let err = result.unwrap_err();
2203 assert!(
2204 err.to_string().contains("maximum loop iterations exceeded"),
2205 "Expected loop limit error, got: {}",
2206 err
2207 );
2208 }
2209
2210 #[tokio::test]
2211 async fn test_loop_iteration_limit_not_exceeded() {
2212 let limits = ExecutionLimits::new().max_loop_iterations(10);
2213 let mut bash = Bash::builder().limits(limits).build();
2214
2215 let result = bash
2217 .exec("for i in 1 2 3 4 5; do echo $i; done")
2218 .await
2219 .unwrap();
2220 assert_eq!(result.stdout, "1\n2\n3\n4\n5\n");
2221 }
2222
2223 #[tokio::test]
2224 async fn test_function_depth_limit() {
2225 let limits = ExecutionLimits::new().max_function_depth(3);
2226 let mut bash = Bash::builder().limits(limits).build();
2227
2228 let result = bash
2230 .exec("f() { echo $1; if [ $1 -lt 5 ]; then f $(($1 + 1)); fi; }; f 1")
2231 .await;
2232 assert!(result.is_err());
2233 let err = result.unwrap_err();
2234 assert!(
2235 err.to_string().contains("maximum function depth exceeded"),
2236 "Expected function depth error, got: {}",
2237 err
2238 );
2239 }
2240
2241 #[tokio::test]
2242 async fn test_function_depth_limit_not_exceeded() {
2243 let limits = ExecutionLimits::new().max_function_depth(10);
2244 let mut bash = Bash::builder().limits(limits).build();
2245
2246 let result = bash.exec("f() { echo hello; }; f").await.unwrap();
2248 assert_eq!(result.stdout, "hello\n");
2249 }
2250
2251 #[tokio::test]
2252 async fn test_while_loop_limit() {
2253 let limits = ExecutionLimits::new().max_loop_iterations(3);
2254 let mut bash = Bash::builder().limits(limits).build();
2255
2256 let result = bash
2258 .exec("i=0; while [ $i -lt 10 ]; do echo $i; i=$((i + 1)); done")
2259 .await;
2260 assert!(result.is_err());
2261 let err = result.unwrap_err();
2262 assert!(
2263 err.to_string().contains("maximum loop iterations exceeded"),
2264 "Expected loop limit error, got: {}",
2265 err
2266 );
2267 }
2268
2269 #[tokio::test]
2270 async fn test_default_limits_allow_normal_scripts() {
2271 let mut bash = Bash::new();
2273 let result = bash
2275 .exec("for i in 1 2 3 4 5; do echo $i; done && echo finished")
2276 .await
2277 .unwrap();
2278 assert_eq!(result.stdout, "1\n2\n3\n4\n5\nfinished\n");
2279 }
2280
2281 #[tokio::test]
2282 async fn test_for_followed_by_echo_done() {
2283 let mut bash = Bash::new();
2284 let result = bash
2285 .exec("for i in 1; do echo $i; done; echo ok")
2286 .await
2287 .unwrap();
2288 assert_eq!(result.stdout, "1\nok\n");
2289 }
2290
2291 #[tokio::test]
2294 async fn test_fs_read_write_binary() {
2295 let bash = Bash::new();
2296 let fs = bash.fs();
2297 let path = std::path::Path::new("/tmp/binary.bin");
2298
2299 let binary_data: Vec<u8> = vec![0x00, 0x01, 0xFF, 0xFE, 0x42, 0x00, 0x7F];
2301 fs.write_file(path, &binary_data).await.unwrap();
2302
2303 let content = fs.read_file(path).await.unwrap();
2305 assert_eq!(content, binary_data);
2306 }
2307
2308 #[tokio::test]
2309 async fn test_fs_write_then_exec_cat() {
2310 let mut bash = Bash::new();
2311 let path = std::path::Path::new("/tmp/prepopulated.txt");
2312
2313 bash.fs()
2315 .write_file(path, b"Hello from Rust!\n")
2316 .await
2317 .unwrap();
2318
2319 let result = bash.exec("cat /tmp/prepopulated.txt").await.unwrap();
2321 assert_eq!(result.stdout, "Hello from Rust!\n");
2322 }
2323
2324 #[tokio::test]
2325 async fn test_fs_exec_then_read() {
2326 let mut bash = Bash::new();
2327 let path = std::path::Path::new("/tmp/from_bash.txt");
2328
2329 bash.exec("echo 'Created by bash' > /tmp/from_bash.txt")
2331 .await
2332 .unwrap();
2333
2334 let content = bash.fs().read_file(path).await.unwrap();
2336 assert_eq!(content, b"Created by bash\n");
2337 }
2338
2339 #[tokio::test]
2340 async fn test_fs_exists_and_stat() {
2341 let bash = Bash::new();
2342 let fs = bash.fs();
2343 let path = std::path::Path::new("/tmp/testfile.txt");
2344
2345 assert!(!fs.exists(path).await.unwrap());
2347
2348 fs.write_file(path, b"content").await.unwrap();
2350
2351 assert!(fs.exists(path).await.unwrap());
2353
2354 let stat = fs.stat(path).await.unwrap();
2356 assert!(stat.file_type.is_file());
2357 assert_eq!(stat.size, 7); }
2359
2360 #[tokio::test]
2361 async fn test_fs_mkdir_and_read_dir() {
2362 let bash = Bash::new();
2363 let fs = bash.fs();
2364
2365 fs.mkdir(std::path::Path::new("/data/nested/dir"), true)
2367 .await
2368 .unwrap();
2369
2370 fs.write_file(std::path::Path::new("/data/file1.txt"), b"1")
2372 .await
2373 .unwrap();
2374 fs.write_file(std::path::Path::new("/data/file2.txt"), b"2")
2375 .await
2376 .unwrap();
2377
2378 let entries = fs.read_dir(std::path::Path::new("/data")).await.unwrap();
2380 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
2381 assert!(names.contains(&"nested"));
2382 assert!(names.contains(&"file1.txt"));
2383 assert!(names.contains(&"file2.txt"));
2384 }
2385
2386 #[tokio::test]
2387 async fn test_fs_append() {
2388 let bash = Bash::new();
2389 let fs = bash.fs();
2390 let path = std::path::Path::new("/tmp/append.txt");
2391
2392 fs.write_file(path, b"line1\n").await.unwrap();
2393 fs.append_file(path, b"line2\n").await.unwrap();
2394 fs.append_file(path, b"line3\n").await.unwrap();
2395
2396 let content = fs.read_file(path).await.unwrap();
2397 assert_eq!(content, b"line1\nline2\nline3\n");
2398 }
2399
2400 #[tokio::test]
2401 async fn test_fs_copy_and_rename() {
2402 let bash = Bash::new();
2403 let fs = bash.fs();
2404
2405 fs.write_file(std::path::Path::new("/tmp/original.txt"), b"data")
2406 .await
2407 .unwrap();
2408
2409 fs.copy(
2411 std::path::Path::new("/tmp/original.txt"),
2412 std::path::Path::new("/tmp/copied.txt"),
2413 )
2414 .await
2415 .unwrap();
2416
2417 fs.rename(
2419 std::path::Path::new("/tmp/copied.txt"),
2420 std::path::Path::new("/tmp/renamed.txt"),
2421 )
2422 .await
2423 .unwrap();
2424
2425 let content = fs
2427 .read_file(std::path::Path::new("/tmp/renamed.txt"))
2428 .await
2429 .unwrap();
2430 assert_eq!(content, b"data");
2431 assert!(!fs
2432 .exists(std::path::Path::new("/tmp/copied.txt"))
2433 .await
2434 .unwrap());
2435 }
2436
2437 #[tokio::test]
2440 async fn test_echo_done_as_argument() {
2441 let mut bash = Bash::new();
2443 let result = bash
2444 .exec("for i in 1; do echo $i; done; echo done")
2445 .await
2446 .unwrap();
2447 assert_eq!(result.stdout, "1\ndone\n");
2448 }
2449
2450 #[tokio::test]
2451 async fn test_simple_echo_done() {
2452 let mut bash = Bash::new();
2454 let result = bash.exec("echo done").await.unwrap();
2455 assert_eq!(result.stdout, "done\n");
2456 }
2457
2458 #[tokio::test]
2459 async fn test_dev_null_redirect() {
2460 let mut bash = Bash::new();
2462 let result = bash.exec("echo hello > /dev/null; echo ok").await.unwrap();
2463 assert_eq!(result.stdout, "ok\n");
2464 }
2465
2466 #[tokio::test]
2467 async fn test_string_concatenation_in_loop() {
2468 let mut bash = Bash::new();
2470 let result = bash.exec("for i in a b c; do echo $i; done").await.unwrap();
2472 assert_eq!(result.stdout, "a\nb\nc\n");
2473
2474 let mut bash = Bash::new();
2476 let result = bash
2477 .exec("result=x; for i in a b c; do echo $i; done; echo $result")
2478 .await
2479 .unwrap();
2480 assert_eq!(result.stdout, "a\nb\nc\nx\n");
2481
2482 let mut bash = Bash::new();
2484 let result = bash
2485 .exec("result=start; for i in a b c; do result=${result}$i; done; echo $result")
2486 .await
2487 .unwrap();
2488 assert_eq!(result.stdout, "startabc\n");
2489 }
2490
2491 #[tokio::test]
2494 async fn test_done_still_terminates_loop() {
2495 let mut bash = Bash::new();
2497 let result = bash.exec("for i in 1 2; do echo $i; done").await.unwrap();
2498 assert_eq!(result.stdout, "1\n2\n");
2499 }
2500
2501 #[tokio::test]
2502 async fn test_fi_still_terminates_if() {
2503 let mut bash = Bash::new();
2505 let result = bash.exec("if true; then echo yes; fi").await.unwrap();
2506 assert_eq!(result.stdout, "yes\n");
2507 }
2508
2509 #[tokio::test]
2510 async fn test_echo_fi_as_argument() {
2511 let mut bash = Bash::new();
2513 let result = bash.exec("echo fi").await.unwrap();
2514 assert_eq!(result.stdout, "fi\n");
2515 }
2516
2517 #[tokio::test]
2518 async fn test_echo_then_as_argument() {
2519 let mut bash = Bash::new();
2521 let result = bash.exec("echo then").await.unwrap();
2522 assert_eq!(result.stdout, "then\n");
2523 }
2524
2525 #[tokio::test]
2526 async fn test_reserved_words_in_quotes_are_arguments() {
2527 let mut bash = Bash::new();
2529 let result = bash.exec("echo 'done' 'fi' 'then'").await.unwrap();
2530 assert_eq!(result.stdout, "done fi then\n");
2531 }
2532
2533 #[tokio::test]
2534 async fn test_nested_loops_done_keyword() {
2535 let mut bash = Bash::new();
2537 let result = bash
2538 .exec("for i in 1; do for j in a; do echo $i$j; done; done")
2539 .await
2540 .unwrap();
2541 assert_eq!(result.stdout, "1a\n");
2542 }
2543
2544 #[tokio::test]
2547 async fn test_dev_null_read_returns_empty() {
2548 let mut bash = Bash::new();
2550 let result = bash.exec("cat /dev/null").await.unwrap();
2551 assert_eq!(result.stdout, "");
2552 }
2553
2554 #[tokio::test]
2555 async fn test_dev_null_append() {
2556 let mut bash = Bash::new();
2558 let result = bash.exec("echo hello >> /dev/null; echo ok").await.unwrap();
2559 assert_eq!(result.stdout, "ok\n");
2560 }
2561
2562 #[tokio::test]
2563 async fn test_dev_null_in_pipeline() {
2564 let mut bash = Bash::new();
2566 let result = bash
2567 .exec("echo hello | cat > /dev/null; echo ok")
2568 .await
2569 .unwrap();
2570 assert_eq!(result.stdout, "ok\n");
2571 }
2572
2573 #[tokio::test]
2574 async fn test_dev_null_exists() {
2575 let mut bash = Bash::new();
2577 let result = bash.exec("cat /dev/null; echo exit_$?").await.unwrap();
2578 assert_eq!(result.stdout, "exit_0\n");
2579 }
2580
2581 #[tokio::test]
2584 async fn test_custom_username_whoami() {
2585 let mut bash = Bash::builder().username("alice").build();
2586 let result = bash.exec("whoami").await.unwrap();
2587 assert_eq!(result.stdout, "alice\n");
2588 }
2589
2590 #[tokio::test]
2591 async fn test_custom_username_id() {
2592 let mut bash = Bash::builder().username("bob").build();
2593 let result = bash.exec("id").await.unwrap();
2594 assert!(result.stdout.contains("uid=1000(bob)"));
2595 assert!(result.stdout.contains("gid=1000(bob)"));
2596 }
2597
2598 #[tokio::test]
2599 async fn test_custom_username_sets_user_env() {
2600 let mut bash = Bash::builder().username("charlie").build();
2601 let result = bash.exec("echo $USER").await.unwrap();
2602 assert_eq!(result.stdout, "charlie\n");
2603 }
2604
2605 #[tokio::test]
2606 async fn test_custom_hostname() {
2607 let mut bash = Bash::builder().hostname("my-server").build();
2608 let result = bash.exec("hostname").await.unwrap();
2609 assert_eq!(result.stdout, "my-server\n");
2610 }
2611
2612 #[tokio::test]
2613 async fn test_custom_hostname_uname() {
2614 let mut bash = Bash::builder().hostname("custom-host").build();
2615 let result = bash.exec("uname -n").await.unwrap();
2616 assert_eq!(result.stdout, "custom-host\n");
2617 }
2618
2619 #[tokio::test]
2620 async fn test_default_username_and_hostname() {
2621 let mut bash = Bash::new();
2623 let result = bash.exec("whoami").await.unwrap();
2624 assert_eq!(result.stdout, "sandbox\n");
2625
2626 let result = bash.exec("hostname").await.unwrap();
2627 assert_eq!(result.stdout, "bashkit-sandbox\n");
2628 }
2629
2630 #[tokio::test]
2631 async fn test_custom_username_and_hostname_combined() {
2632 let mut bash = Bash::builder()
2633 .username("deploy")
2634 .hostname("prod-server-01")
2635 .build();
2636
2637 let result = bash.exec("whoami && hostname").await.unwrap();
2638 assert_eq!(result.stdout, "deploy\nprod-server-01\n");
2639
2640 let result = bash.exec("echo $USER").await.unwrap();
2641 assert_eq!(result.stdout, "deploy\n");
2642 }
2643
2644 mod custom_builtins {
2647 use super::*;
2648 use crate::builtins::{Builtin, Context};
2649 use crate::ExecResult;
2650 use async_trait::async_trait;
2651
2652 struct Hello;
2654
2655 #[async_trait]
2656 impl Builtin for Hello {
2657 async fn execute(&self, _ctx: Context<'_>) -> crate::Result<ExecResult> {
2658 Ok(ExecResult::ok("Hello from custom builtin!\n".to_string()))
2659 }
2660 }
2661
2662 #[tokio::test]
2663 async fn test_custom_builtin_basic() {
2664 let mut bash = Bash::builder().builtin("hello", Box::new(Hello)).build();
2665
2666 let result = bash.exec("hello").await.unwrap();
2667 assert_eq!(result.stdout, "Hello from custom builtin!\n");
2668 assert_eq!(result.exit_code, 0);
2669 }
2670
2671 struct Greet;
2673
2674 #[async_trait]
2675 impl Builtin for Greet {
2676 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2677 let name = ctx.args.first().map(|s| s.as_str()).unwrap_or("World");
2678 Ok(ExecResult::ok(format!("Hello, {}!\n", name)))
2679 }
2680 }
2681
2682 #[tokio::test]
2683 async fn test_custom_builtin_with_args() {
2684 let mut bash = Bash::builder().builtin("greet", Box::new(Greet)).build();
2685
2686 let result = bash.exec("greet").await.unwrap();
2687 assert_eq!(result.stdout, "Hello, World!\n");
2688
2689 let result = bash.exec("greet Alice").await.unwrap();
2690 assert_eq!(result.stdout, "Hello, Alice!\n");
2691
2692 let result = bash.exec("greet Bob Charlie").await.unwrap();
2693 assert_eq!(result.stdout, "Hello, Bob!\n");
2694 }
2695
2696 struct Upper;
2698
2699 #[async_trait]
2700 impl Builtin for Upper {
2701 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2702 let input = ctx.stdin.unwrap_or("");
2703 Ok(ExecResult::ok(input.to_uppercase()))
2704 }
2705 }
2706
2707 #[tokio::test]
2708 async fn test_custom_builtin_with_stdin() {
2709 let mut bash = Bash::builder().builtin("upper", Box::new(Upper)).build();
2710
2711 let result = bash.exec("echo hello | upper").await.unwrap();
2712 assert_eq!(result.stdout, "HELLO\n");
2713 }
2714
2715 struct WriteFile;
2717
2718 #[async_trait]
2719 impl Builtin for WriteFile {
2720 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2721 if ctx.args.len() < 2 {
2722 return Ok(ExecResult::err(
2723 "Usage: writefile <path> <content>\n".to_string(),
2724 1,
2725 ));
2726 }
2727 let path = std::path::Path::new(&ctx.args[0]);
2728 let content = ctx.args[1..].join(" ");
2729 ctx.fs.write_file(path, content.as_bytes()).await?;
2730 Ok(ExecResult::ok(String::new()))
2731 }
2732 }
2733
2734 #[tokio::test]
2735 async fn test_custom_builtin_with_filesystem() {
2736 let mut bash = Bash::builder()
2737 .builtin("writefile", Box::new(WriteFile))
2738 .build();
2739
2740 bash.exec("writefile /tmp/test.txt custom content here")
2741 .await
2742 .unwrap();
2743
2744 let result = bash.exec("cat /tmp/test.txt").await.unwrap();
2745 assert_eq!(result.stdout, "custom content here");
2746 }
2747
2748 struct CustomEcho;
2750
2751 #[async_trait]
2752 impl Builtin for CustomEcho {
2753 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2754 let msg = ctx.args.join(" ");
2755 Ok(ExecResult::ok(format!("[CUSTOM] {}\n", msg)))
2756 }
2757 }
2758
2759 #[tokio::test]
2760 async fn test_custom_builtin_override_default() {
2761 let mut bash = Bash::builder()
2762 .builtin("echo", Box::new(CustomEcho))
2763 .build();
2764
2765 let result = bash.exec("echo hello world").await.unwrap();
2766 assert_eq!(result.stdout, "[CUSTOM] hello world\n");
2767 }
2768
2769 #[tokio::test]
2771 async fn test_multiple_custom_builtins() {
2772 let mut bash = Bash::builder()
2773 .builtin("hello", Box::new(Hello))
2774 .builtin("greet", Box::new(Greet))
2775 .builtin("upper", Box::new(Upper))
2776 .build();
2777
2778 let result = bash.exec("hello").await.unwrap();
2779 assert_eq!(result.stdout, "Hello from custom builtin!\n");
2780
2781 let result = bash.exec("greet Test").await.unwrap();
2782 assert_eq!(result.stdout, "Hello, Test!\n");
2783
2784 let result = bash.exec("echo foo | upper").await.unwrap();
2785 assert_eq!(result.stdout, "FOO\n");
2786 }
2787
2788 struct Counter {
2790 prefix: String,
2791 }
2792
2793 #[async_trait]
2794 impl Builtin for Counter {
2795 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2796 let count = ctx
2797 .args
2798 .first()
2799 .and_then(|s| s.parse::<i32>().ok())
2800 .unwrap_or(1);
2801 let mut output = String::new();
2802 for i in 1..=count {
2803 output.push_str(&format!("{}{}\n", self.prefix, i));
2804 }
2805 Ok(ExecResult::ok(output))
2806 }
2807 }
2808
2809 #[tokio::test]
2810 async fn test_custom_builtin_with_state() {
2811 let mut bash = Bash::builder()
2812 .builtin(
2813 "count",
2814 Box::new(Counter {
2815 prefix: "Item ".to_string(),
2816 }),
2817 )
2818 .build();
2819
2820 let result = bash.exec("count 3").await.unwrap();
2821 assert_eq!(result.stdout, "Item 1\nItem 2\nItem 3\n");
2822 }
2823
2824 struct Fail;
2826
2827 #[async_trait]
2828 impl Builtin for Fail {
2829 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2830 let code = ctx
2831 .args
2832 .first()
2833 .and_then(|s| s.parse::<i32>().ok())
2834 .unwrap_or(1);
2835 Ok(ExecResult::err(
2836 format!("Failed with code {}\n", code),
2837 code,
2838 ))
2839 }
2840 }
2841
2842 #[tokio::test]
2843 async fn test_custom_builtin_error() {
2844 let mut bash = Bash::builder().builtin("fail", Box::new(Fail)).build();
2845
2846 let result = bash.exec("fail 42").await.unwrap();
2847 assert_eq!(result.exit_code, 42);
2848 assert_eq!(result.stderr, "Failed with code 42\n");
2849 }
2850
2851 #[tokio::test]
2852 async fn test_custom_builtin_in_script() {
2853 let mut bash = Bash::builder().builtin("greet", Box::new(Greet)).build();
2854
2855 let script = r#"
2856 for name in Alice Bob Charlie; do
2857 greet $name
2858 done
2859 "#;
2860
2861 let result = bash.exec(script).await.unwrap();
2862 assert_eq!(
2863 result.stdout,
2864 "Hello, Alice!\nHello, Bob!\nHello, Charlie!\n"
2865 );
2866 }
2867
2868 #[tokio::test]
2869 async fn test_custom_builtin_with_conditionals() {
2870 let mut bash = Bash::builder()
2871 .builtin("fail", Box::new(Fail))
2872 .builtin("hello", Box::new(Hello))
2873 .build();
2874
2875 let result = bash.exec("fail 1 || hello").await.unwrap();
2876 assert_eq!(result.stdout, "Hello from custom builtin!\n");
2877 assert_eq!(result.exit_code, 0);
2878
2879 let result = bash.exec("hello && fail 5").await.unwrap();
2880 assert_eq!(result.exit_code, 5);
2881 }
2882
2883 struct EnvReader;
2885
2886 #[async_trait]
2887 impl Builtin for EnvReader {
2888 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2889 let var_name = ctx.args.first().map(|s| s.as_str()).unwrap_or("HOME");
2890 let value = ctx
2891 .env
2892 .get(var_name)
2893 .map(|s| s.as_str())
2894 .unwrap_or("(not set)");
2895 Ok(ExecResult::ok(format!("{}={}\n", var_name, value)))
2896 }
2897 }
2898
2899 #[tokio::test]
2900 async fn test_custom_builtin_reads_env() {
2901 let mut bash = Bash::builder()
2902 .env("MY_VAR", "my_value")
2903 .builtin("readenv", Box::new(EnvReader))
2904 .build();
2905
2906 let result = bash.exec("readenv MY_VAR").await.unwrap();
2907 assert_eq!(result.stdout, "MY_VAR=my_value\n");
2908
2909 let result = bash.exec("readenv UNKNOWN").await.unwrap();
2910 assert_eq!(result.stdout, "UNKNOWN=(not set)\n");
2911 }
2912 }
2913
2914 #[tokio::test]
2917 async fn test_parser_timeout_default() {
2918 let limits = ExecutionLimits::default();
2920 assert_eq!(limits.parser_timeout, std::time::Duration::from_secs(5));
2921 }
2922
2923 #[tokio::test]
2924 async fn test_parser_timeout_custom() {
2925 let limits = ExecutionLimits::new().parser_timeout(std::time::Duration::from_millis(100));
2927 assert_eq!(limits.parser_timeout, std::time::Duration::from_millis(100));
2928 }
2929
2930 #[tokio::test]
2931 async fn test_parser_timeout_normal_script() {
2932 let limits = ExecutionLimits::new().parser_timeout(std::time::Duration::from_secs(1));
2934 let mut bash = Bash::builder().limits(limits).build();
2935 let result = bash.exec("echo hello").await.unwrap();
2936 assert_eq!(result.stdout, "hello\n");
2937 }
2938
2939 #[tokio::test]
2942 async fn test_parser_fuel_default() {
2943 let limits = ExecutionLimits::default();
2945 assert_eq!(limits.max_parser_operations, 100_000);
2946 }
2947
2948 #[tokio::test]
2949 async fn test_parser_fuel_custom() {
2950 let limits = ExecutionLimits::new().max_parser_operations(1000);
2952 assert_eq!(limits.max_parser_operations, 1000);
2953 }
2954
2955 #[tokio::test]
2956 async fn test_parser_fuel_normal_script() {
2957 let limits = ExecutionLimits::new().max_parser_operations(1000);
2959 let mut bash = Bash::builder().limits(limits).build();
2960 let result = bash.exec("echo hello").await.unwrap();
2961 assert_eq!(result.stdout, "hello\n");
2962 }
2963
2964 #[tokio::test]
2967 async fn test_input_size_limit_default() {
2968 let limits = ExecutionLimits::default();
2970 assert_eq!(limits.max_input_bytes, 10_000_000);
2971 }
2972
2973 #[tokio::test]
2974 async fn test_input_size_limit_custom() {
2975 let limits = ExecutionLimits::new().max_input_bytes(1000);
2977 assert_eq!(limits.max_input_bytes, 1000);
2978 }
2979
2980 #[tokio::test]
2981 async fn test_input_size_limit_enforced() {
2982 let limits = ExecutionLimits::new().max_input_bytes(10);
2984 let mut bash = Bash::builder().limits(limits).build();
2985
2986 let result = bash.exec("echo hello world").await;
2988 assert!(result.is_err());
2989 let err = result.unwrap_err();
2990 assert!(
2991 err.to_string().contains("input too large"),
2992 "Expected input size error, got: {}",
2993 err
2994 );
2995 }
2996
2997 #[tokio::test]
2998 async fn test_input_size_limit_normal_script() {
2999 let limits = ExecutionLimits::new().max_input_bytes(1000);
3001 let mut bash = Bash::builder().limits(limits).build();
3002 let result = bash.exec("echo hello").await.unwrap();
3003 assert_eq!(result.stdout, "hello\n");
3004 }
3005
3006 #[tokio::test]
3009 async fn test_ast_depth_limit_default() {
3010 let limits = ExecutionLimits::default();
3012 assert_eq!(limits.max_ast_depth, 100);
3013 }
3014
3015 #[tokio::test]
3016 async fn test_ast_depth_limit_custom() {
3017 let limits = ExecutionLimits::new().max_ast_depth(10);
3019 assert_eq!(limits.max_ast_depth, 10);
3020 }
3021
3022 #[tokio::test]
3023 async fn test_ast_depth_limit_normal_script() {
3024 let limits = ExecutionLimits::new().max_ast_depth(10);
3026 let mut bash = Bash::builder().limits(limits).build();
3027 let result = bash.exec("if true; then echo ok; fi").await.unwrap();
3028 assert_eq!(result.stdout, "ok\n");
3029 }
3030
3031 #[tokio::test]
3032 async fn test_ast_depth_limit_enforced() {
3033 let limits = ExecutionLimits::new().max_ast_depth(2);
3035 let mut bash = Bash::builder().limits(limits).build();
3036
3037 let result = bash
3039 .exec("if true; then if true; then if true; then echo nested; fi; fi; fi")
3040 .await;
3041 assert!(result.is_err());
3042 let err = result.unwrap_err();
3043 assert!(
3044 err.to_string().contains("AST nesting too deep"),
3045 "Expected AST depth error, got: {}",
3046 err
3047 );
3048 }
3049
3050 #[tokio::test]
3051 async fn test_parser_fuel_enforced() {
3052 let limits = ExecutionLimits::new().max_parser_operations(3);
3055 let mut bash = Bash::builder().limits(limits).build();
3056
3057 let result = bash.exec("echo a; echo b; echo c").await;
3059 assert!(result.is_err());
3060 let err = result.unwrap_err();
3061 assert!(
3062 err.to_string().contains("parser fuel exhausted"),
3063 "Expected parser fuel error, got: {}",
3064 err
3065 );
3066 }
3067
3068 #[tokio::test]
3071 async fn test_set_e_basic() {
3072 let mut bash = Bash::new();
3074 let result = bash
3075 .exec("set -e; true; false; echo should_not_reach")
3076 .await
3077 .unwrap();
3078 assert_eq!(result.stdout, "");
3079 assert_eq!(result.exit_code, 1);
3080 }
3081
3082 #[tokio::test]
3083 async fn test_set_e_after_failing_cmd() {
3084 let mut bash = Bash::new();
3086 let result = bash
3087 .exec("set -e; echo before; false; echo after")
3088 .await
3089 .unwrap();
3090 assert_eq!(result.stdout, "before\n");
3091 assert_eq!(result.exit_code, 1);
3092 }
3093
3094 #[tokio::test]
3095 async fn test_set_e_disabled() {
3096 let mut bash = Bash::new();
3098 let result = bash
3099 .exec("set -e; set +e; false; echo still_running")
3100 .await
3101 .unwrap();
3102 assert_eq!(result.stdout, "still_running\n");
3103 }
3104
3105 #[tokio::test]
3106 async fn test_set_e_in_pipeline_last() {
3107 let mut bash = Bash::new();
3109 let result = bash
3110 .exec("set -e; false | true; echo reached")
3111 .await
3112 .unwrap();
3113 assert_eq!(result.stdout, "reached\n");
3114 }
3115
3116 #[tokio::test]
3117 async fn test_set_e_in_if_condition() {
3118 let mut bash = Bash::new();
3120 let result = bash
3121 .exec("set -e; if false; then echo yes; else echo no; fi; echo done")
3122 .await
3123 .unwrap();
3124 assert_eq!(result.stdout, "no\ndone\n");
3125 }
3126
3127 #[tokio::test]
3128 async fn test_set_e_in_while_condition() {
3129 let mut bash = Bash::new();
3131 let result = bash
3132 .exec("set -e; x=0; while [ \"$x\" -lt 2 ]; do echo \"x=$x\"; x=$((x + 1)); done; echo done")
3133 .await
3134 .unwrap();
3135 assert_eq!(result.stdout, "x=0\nx=1\ndone\n");
3136 }
3137
3138 #[tokio::test]
3139 async fn test_set_e_in_brace_group() {
3140 let mut bash = Bash::new();
3142 let result = bash
3143 .exec("set -e; { echo start; false; echo unreached; }; echo after")
3144 .await
3145 .unwrap();
3146 assert_eq!(result.stdout, "start\n");
3147 assert_eq!(result.exit_code, 1);
3148 }
3149
3150 #[tokio::test]
3151 async fn test_set_e_and_chain() {
3152 let mut bash = Bash::new();
3154 let result = bash
3155 .exec("set -e; false && echo one; echo reached")
3156 .await
3157 .unwrap();
3158 assert_eq!(result.stdout, "reached\n");
3159 }
3160
3161 #[tokio::test]
3162 async fn test_set_e_or_chain() {
3163 let mut bash = Bash::new();
3165 let result = bash
3166 .exec("set -e; true || false; echo reached")
3167 .await
3168 .unwrap();
3169 assert_eq!(result.stdout, "reached\n");
3170 }
3171
3172 #[tokio::test]
3175 async fn test_tilde_expansion_basic() {
3176 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3178 let result = bash.exec("echo ~").await.unwrap();
3179 assert_eq!(result.stdout, "/home/testuser\n");
3180 }
3181
3182 #[tokio::test]
3183 async fn test_tilde_expansion_with_path() {
3184 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3186 let result = bash.exec("echo ~/documents/file.txt").await.unwrap();
3187 assert_eq!(result.stdout, "/home/testuser/documents/file.txt\n");
3188 }
3189
3190 #[tokio::test]
3191 async fn test_tilde_expansion_in_assignment() {
3192 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3194 let result = bash.exec("DIR=~/data; echo $DIR").await.unwrap();
3195 assert_eq!(result.stdout, "/home/testuser/data\n");
3196 }
3197
3198 #[tokio::test]
3199 async fn test_tilde_expansion_default_home() {
3200 let mut bash = Bash::new();
3202 let result = bash.exec("echo ~").await.unwrap();
3203 assert_eq!(result.stdout, "/home/sandbox\n");
3204 }
3205
3206 #[tokio::test]
3207 async fn test_tilde_not_at_start() {
3208 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3210 let result = bash.exec("echo foo~bar").await.unwrap();
3211 assert_eq!(result.stdout, "foo~bar\n");
3212 }
3213
3214 #[tokio::test]
3217 async fn test_special_var_dollar_dollar() {
3218 let mut bash = Bash::new();
3220 let result = bash.exec("echo $$").await.unwrap();
3221 let pid: u32 = result.stdout.trim().parse().expect("$$ should be a number");
3223 assert!(pid > 0, "$$ should be a positive number");
3224 }
3225
3226 #[tokio::test]
3227 async fn test_special_var_random() {
3228 let mut bash = Bash::new();
3230 let result = bash.exec("echo $RANDOM").await.unwrap();
3231 let random: u32 = result
3232 .stdout
3233 .trim()
3234 .parse()
3235 .expect("$RANDOM should be a number");
3236 assert!(random < 32768, "$RANDOM should be < 32768");
3237 }
3238
3239 #[tokio::test]
3240 async fn test_special_var_random_varies() {
3241 let mut bash = Bash::new();
3243 let result1 = bash.exec("echo $RANDOM").await.unwrap();
3244 let result2 = bash.exec("echo $RANDOM").await.unwrap();
3245 let _: u32 = result1
3249 .stdout
3250 .trim()
3251 .parse()
3252 .expect("$RANDOM should be a number");
3253 let _: u32 = result2
3254 .stdout
3255 .trim()
3256 .parse()
3257 .expect("$RANDOM should be a number");
3258 }
3259
3260 #[tokio::test]
3261 async fn test_special_var_lineno() {
3262 let mut bash = Bash::new();
3264 let result = bash.exec("echo $LINENO").await.unwrap();
3265 assert_eq!(result.stdout, "1\n");
3266 }
3267
3268 #[tokio::test]
3269 async fn test_lineno_multiline() {
3270 let mut bash = Bash::new();
3272 let result = bash
3273 .exec(
3274 r#"echo "line $LINENO"
3275echo "line $LINENO"
3276echo "line $LINENO""#,
3277 )
3278 .await
3279 .unwrap();
3280 assert_eq!(result.stdout, "line 1\nline 2\nline 3\n");
3281 }
3282
3283 #[tokio::test]
3284 async fn test_lineno_in_loop() {
3285 let mut bash = Bash::new();
3287 let result = bash
3288 .exec(
3289 r#"for i in 1 2; do
3290 echo "loop $LINENO"
3291done"#,
3292 )
3293 .await
3294 .unwrap();
3295 assert_eq!(result.stdout, "loop 2\nloop 2\n");
3297 }
3298
3299 #[tokio::test]
3302 async fn test_file_test_r_readable() {
3303 let mut bash = Bash::new();
3305 bash.exec("echo hello > /tmp/readable.txt").await.unwrap();
3306 let result = bash
3307 .exec("test -r /tmp/readable.txt && echo yes")
3308 .await
3309 .unwrap();
3310 assert_eq!(result.stdout, "yes\n");
3311 }
3312
3313 #[tokio::test]
3314 async fn test_file_test_r_not_exists() {
3315 let mut bash = Bash::new();
3317 let result = bash
3318 .exec("test -r /tmp/nonexistent.txt && echo yes || echo no")
3319 .await
3320 .unwrap();
3321 assert_eq!(result.stdout, "no\n");
3322 }
3323
3324 #[tokio::test]
3325 async fn test_file_test_w_writable() {
3326 let mut bash = Bash::new();
3328 bash.exec("echo hello > /tmp/writable.txt").await.unwrap();
3329 let result = bash
3330 .exec("test -w /tmp/writable.txt && echo yes")
3331 .await
3332 .unwrap();
3333 assert_eq!(result.stdout, "yes\n");
3334 }
3335
3336 #[tokio::test]
3337 async fn test_file_test_x_executable() {
3338 let mut bash = Bash::new();
3340 bash.exec("echo '#!/bin/bash' > /tmp/script.sh")
3341 .await
3342 .unwrap();
3343 bash.exec("chmod 755 /tmp/script.sh").await.unwrap();
3344 let result = bash
3345 .exec("test -x /tmp/script.sh && echo yes")
3346 .await
3347 .unwrap();
3348 assert_eq!(result.stdout, "yes\n");
3349 }
3350
3351 #[tokio::test]
3352 async fn test_file_test_x_not_executable() {
3353 let mut bash = Bash::new();
3355 bash.exec("echo 'data' > /tmp/noexec.txt").await.unwrap();
3356 bash.exec("chmod 644 /tmp/noexec.txt").await.unwrap();
3357 let result = bash
3358 .exec("test -x /tmp/noexec.txt && echo yes || echo no")
3359 .await
3360 .unwrap();
3361 assert_eq!(result.stdout, "no\n");
3362 }
3363
3364 #[tokio::test]
3365 async fn test_file_test_e_exists() {
3366 let mut bash = Bash::new();
3368 bash.exec("echo hello > /tmp/exists.txt").await.unwrap();
3369 let result = bash
3370 .exec("test -e /tmp/exists.txt && echo yes")
3371 .await
3372 .unwrap();
3373 assert_eq!(result.stdout, "yes\n");
3374 }
3375
3376 #[tokio::test]
3377 async fn test_file_test_f_regular() {
3378 let mut bash = Bash::new();
3380 bash.exec("echo hello > /tmp/regular.txt").await.unwrap();
3381 let result = bash
3382 .exec("test -f /tmp/regular.txt && echo yes")
3383 .await
3384 .unwrap();
3385 assert_eq!(result.stdout, "yes\n");
3386 }
3387
3388 #[tokio::test]
3389 async fn test_file_test_d_directory() {
3390 let mut bash = Bash::new();
3392 bash.exec("mkdir -p /tmp/mydir").await.unwrap();
3393 let result = bash.exec("test -d /tmp/mydir && echo yes").await.unwrap();
3394 assert_eq!(result.stdout, "yes\n");
3395 }
3396
3397 #[tokio::test]
3398 async fn test_file_test_s_size() {
3399 let mut bash = Bash::new();
3401 bash.exec("echo hello > /tmp/nonempty.txt").await.unwrap();
3402 let result = bash
3403 .exec("test -s /tmp/nonempty.txt && echo yes")
3404 .await
3405 .unwrap();
3406 assert_eq!(result.stdout, "yes\n");
3407 }
3408
3409 #[tokio::test]
3414 async fn test_redirect_both_stdout_stderr() {
3415 let mut bash = Bash::new();
3417 let result = bash.exec("echo hello &> /tmp/out.txt").await.unwrap();
3419 assert_eq!(result.stdout, "");
3421 let check = bash.exec("cat /tmp/out.txt").await.unwrap();
3423 assert_eq!(check.stdout, "hello\n");
3424 }
3425
3426 #[tokio::test]
3427 async fn test_stderr_redirect_to_file() {
3428 let mut bash = Bash::new();
3432 bash.exec("echo stdout; echo stderr 2> /tmp/err.txt")
3434 .await
3435 .unwrap();
3436 }
3439
3440 #[tokio::test]
3441 async fn test_fd_redirect_parsing() {
3442 let mut bash = Bash::new();
3444 let result = bash.exec("true 2> /tmp/err.txt").await.unwrap();
3446 assert_eq!(result.exit_code, 0);
3447 }
3448
3449 #[tokio::test]
3450 async fn test_fd_redirect_append_parsing() {
3451 let mut bash = Bash::new();
3453 let result = bash.exec("true 2>> /tmp/err.txt").await.unwrap();
3454 assert_eq!(result.exit_code, 0);
3455 }
3456
3457 #[tokio::test]
3458 async fn test_fd_dup_parsing() {
3459 let mut bash = Bash::new();
3461 let result = bash.exec("echo hello 2>&1").await.unwrap();
3462 assert_eq!(result.stdout, "hello\n");
3463 assert_eq!(result.exit_code, 0);
3464 }
3465
3466 #[tokio::test]
3467 async fn test_dup_output_redirect_stdout_to_stderr() {
3468 let mut bash = Bash::new();
3470 let result = bash.exec("echo hello >&2").await.unwrap();
3471 assert_eq!(result.stdout, "");
3473 assert_eq!(result.stderr, "hello\n");
3474 }
3475
3476 #[tokio::test]
3477 async fn test_lexer_redirect_both() {
3478 let mut bash = Bash::new();
3480 let result = bash.exec("echo test &> /tmp/both.txt").await.unwrap();
3482 assert_eq!(result.stdout, "");
3483 let check = bash.exec("cat /tmp/both.txt").await.unwrap();
3484 assert_eq!(check.stdout, "test\n");
3485 }
3486
3487 #[tokio::test]
3488 async fn test_lexer_dup_output() {
3489 let mut bash = Bash::new();
3491 let result = bash.exec("echo test >&2").await.unwrap();
3492 assert_eq!(result.stdout, "");
3493 assert_eq!(result.stderr, "test\n");
3494 }
3495
3496 #[tokio::test]
3497 async fn test_digit_before_redirect() {
3498 let mut bash = Bash::new();
3500 let result = bash.exec("echo hello 2> /tmp/err.txt").await.unwrap();
3502 assert_eq!(result.exit_code, 0);
3503 assert_eq!(result.stdout, "hello\n");
3505 }
3506
3507 #[tokio::test]
3512 async fn test_arithmetic_logical_and_true() {
3513 let mut bash = Bash::new();
3515 let result = bash.exec("echo $((1 && 1))").await.unwrap();
3516 assert_eq!(result.stdout, "1\n");
3517 }
3518
3519 #[tokio::test]
3520 async fn test_arithmetic_logical_and_false_left() {
3521 let mut bash = Bash::new();
3523 let result = bash.exec("echo $((0 && 1))").await.unwrap();
3524 assert_eq!(result.stdout, "0\n");
3525 }
3526
3527 #[tokio::test]
3528 async fn test_arithmetic_logical_and_false_right() {
3529 let mut bash = Bash::new();
3531 let result = bash.exec("echo $((1 && 0))").await.unwrap();
3532 assert_eq!(result.stdout, "0\n");
3533 }
3534
3535 #[tokio::test]
3536 async fn test_arithmetic_logical_or_false() {
3537 let mut bash = Bash::new();
3539 let result = bash.exec("echo $((0 || 0))").await.unwrap();
3540 assert_eq!(result.stdout, "0\n");
3541 }
3542
3543 #[tokio::test]
3544 async fn test_arithmetic_logical_or_true_left() {
3545 let mut bash = Bash::new();
3547 let result = bash.exec("echo $((1 || 0))").await.unwrap();
3548 assert_eq!(result.stdout, "1\n");
3549 }
3550
3551 #[tokio::test]
3552 async fn test_arithmetic_logical_or_true_right() {
3553 let mut bash = Bash::new();
3555 let result = bash.exec("echo $((0 || 1))").await.unwrap();
3556 assert_eq!(result.stdout, "1\n");
3557 }
3558
3559 #[tokio::test]
3560 async fn test_arithmetic_logical_combined() {
3561 let mut bash = Bash::new();
3563 let result = bash.exec("echo $((5 > 3 && 2 < 4))").await.unwrap();
3565 assert_eq!(result.stdout, "1\n");
3566 }
3567
3568 #[tokio::test]
3569 async fn test_arithmetic_logical_with_comparison() {
3570 let mut bash = Bash::new();
3572 let result = bash.exec("echo $((5 < 3 || 2 < 4))").await.unwrap();
3574 assert_eq!(result.stdout, "1\n");
3575 }
3576
3577 #[tokio::test]
3582 async fn test_brace_expansion_list() {
3583 let mut bash = Bash::new();
3585 let result = bash.exec("echo {a,b,c}").await.unwrap();
3586 assert_eq!(result.stdout, "a b c\n");
3587 }
3588
3589 #[tokio::test]
3590 async fn test_brace_expansion_with_prefix() {
3591 let mut bash = Bash::new();
3593 let result = bash.exec("echo file{1,2,3}.txt").await.unwrap();
3594 assert_eq!(result.stdout, "file1.txt file2.txt file3.txt\n");
3595 }
3596
3597 #[tokio::test]
3598 async fn test_brace_expansion_numeric_range() {
3599 let mut bash = Bash::new();
3601 let result = bash.exec("echo {1..5}").await.unwrap();
3602 assert_eq!(result.stdout, "1 2 3 4 5\n");
3603 }
3604
3605 #[tokio::test]
3606 async fn test_brace_expansion_char_range() {
3607 let mut bash = Bash::new();
3609 let result = bash.exec("echo {a..e}").await.unwrap();
3610 assert_eq!(result.stdout, "a b c d e\n");
3611 }
3612
3613 #[tokio::test]
3614 async fn test_brace_expansion_reverse_range() {
3615 let mut bash = Bash::new();
3617 let result = bash.exec("echo {5..1}").await.unwrap();
3618 assert_eq!(result.stdout, "5 4 3 2 1\n");
3619 }
3620
3621 #[tokio::test]
3622 async fn test_brace_expansion_nested() {
3623 let mut bash = Bash::new();
3625 let result = bash.exec("echo {a,b}{1,2}").await.unwrap();
3626 assert_eq!(result.stdout, "a1 a2 b1 b2\n");
3627 }
3628
3629 #[tokio::test]
3630 async fn test_brace_expansion_with_suffix() {
3631 let mut bash = Bash::new();
3633 let result = bash.exec("echo pre{x,y}suf").await.unwrap();
3634 assert_eq!(result.stdout, "prexsuf preysuf\n");
3635 }
3636
3637 #[tokio::test]
3638 async fn test_brace_expansion_empty_item() {
3639 let mut bash = Bash::new();
3641 let result = bash.exec("echo x{,y}z").await.unwrap();
3642 assert_eq!(result.stdout, "xz xyz\n");
3643 }
3644
3645 #[tokio::test]
3650 async fn test_string_less_than() {
3651 let mut bash = Bash::new();
3652 let result = bash
3653 .exec("test apple '<' banana && echo yes")
3654 .await
3655 .unwrap();
3656 assert_eq!(result.stdout, "yes\n");
3657 }
3658
3659 #[tokio::test]
3660 async fn test_string_greater_than() {
3661 let mut bash = Bash::new();
3662 let result = bash
3663 .exec("test banana '>' apple && echo yes")
3664 .await
3665 .unwrap();
3666 assert_eq!(result.stdout, "yes\n");
3667 }
3668
3669 #[tokio::test]
3670 async fn test_string_less_than_false() {
3671 let mut bash = Bash::new();
3672 let result = bash
3673 .exec("test banana '<' apple && echo yes || echo no")
3674 .await
3675 .unwrap();
3676 assert_eq!(result.stdout, "no\n");
3677 }
3678
3679 #[tokio::test]
3684 async fn test_array_indices_basic() {
3685 let mut bash = Bash::new();
3687 let result = bash.exec("arr=(a b c); echo ${!arr[@]}").await.unwrap();
3688 assert_eq!(result.stdout, "0 1 2\n");
3689 }
3690
3691 #[tokio::test]
3692 async fn test_array_indices_sparse() {
3693 let mut bash = Bash::new();
3695 let result = bash
3696 .exec("arr[0]=a; arr[5]=b; arr[10]=c; echo ${!arr[@]}")
3697 .await
3698 .unwrap();
3699 assert_eq!(result.stdout, "0 5 10\n");
3700 }
3701
3702 #[tokio::test]
3703 async fn test_array_indices_star() {
3704 let mut bash = Bash::new();
3706 let result = bash.exec("arr=(x y z); echo ${!arr[*]}").await.unwrap();
3707 assert_eq!(result.stdout, "0 1 2\n");
3708 }
3709
3710 #[tokio::test]
3711 async fn test_array_indices_empty() {
3712 let mut bash = Bash::new();
3714 let result = bash.exec("arr=(); echo \"${!arr[@]}\"").await.unwrap();
3715 assert_eq!(result.stdout, "\n");
3716 }
3717
3718 #[tokio::test]
3723 async fn test_text_file_basic() {
3724 let mut bash = Bash::builder()
3725 .mount_text("/config/app.conf", "debug=true\nport=8080\n")
3726 .build();
3727
3728 let result = bash.exec("cat /config/app.conf").await.unwrap();
3729 assert_eq!(result.stdout, "debug=true\nport=8080\n");
3730 }
3731
3732 #[tokio::test]
3733 async fn test_text_file_multiple() {
3734 let mut bash = Bash::builder()
3735 .mount_text("/data/file1.txt", "content one")
3736 .mount_text("/data/file2.txt", "content two")
3737 .mount_text("/other/file3.txt", "content three")
3738 .build();
3739
3740 let result = bash.exec("cat /data/file1.txt").await.unwrap();
3741 assert_eq!(result.stdout, "content one");
3742
3743 let result = bash.exec("cat /data/file2.txt").await.unwrap();
3744 assert_eq!(result.stdout, "content two");
3745
3746 let result = bash.exec("cat /other/file3.txt").await.unwrap();
3747 assert_eq!(result.stdout, "content three");
3748 }
3749
3750 #[tokio::test]
3751 async fn test_text_file_nested_directory() {
3752 let mut bash = Bash::builder()
3754 .mount_text("/a/b/c/d/file.txt", "nested content")
3755 .build();
3756
3757 let result = bash.exec("cat /a/b/c/d/file.txt").await.unwrap();
3758 assert_eq!(result.stdout, "nested content");
3759 }
3760
3761 #[tokio::test]
3762 async fn test_text_file_mode() {
3763 let bash = Bash::builder()
3764 .mount_text("/tmp/writable.txt", "content")
3765 .build();
3766
3767 let stat = bash
3768 .fs()
3769 .stat(std::path::Path::new("/tmp/writable.txt"))
3770 .await
3771 .unwrap();
3772 assert_eq!(stat.mode, 0o644);
3773 }
3774
3775 #[tokio::test]
3776 async fn test_readonly_text_basic() {
3777 let mut bash = Bash::builder()
3778 .mount_readonly_text("/etc/version", "1.2.3")
3779 .build();
3780
3781 let result = bash.exec("cat /etc/version").await.unwrap();
3782 assert_eq!(result.stdout, "1.2.3");
3783 }
3784
3785 #[tokio::test]
3786 async fn test_readonly_text_mode() {
3787 let bash = Bash::builder()
3788 .mount_readonly_text("/etc/readonly.conf", "immutable")
3789 .build();
3790
3791 let stat = bash
3792 .fs()
3793 .stat(std::path::Path::new("/etc/readonly.conf"))
3794 .await
3795 .unwrap();
3796 assert_eq!(stat.mode, 0o444);
3797 }
3798
3799 #[tokio::test]
3800 async fn test_text_file_mixed_readonly_writable() {
3801 let bash = Bash::builder()
3802 .mount_text("/data/writable.txt", "can edit")
3803 .mount_readonly_text("/data/readonly.txt", "cannot edit")
3804 .build();
3805
3806 let writable_stat = bash
3807 .fs()
3808 .stat(std::path::Path::new("/data/writable.txt"))
3809 .await
3810 .unwrap();
3811 let readonly_stat = bash
3812 .fs()
3813 .stat(std::path::Path::new("/data/readonly.txt"))
3814 .await
3815 .unwrap();
3816
3817 assert_eq!(writable_stat.mode, 0o644);
3818 assert_eq!(readonly_stat.mode, 0o444);
3819 }
3820
3821 #[tokio::test]
3822 async fn test_text_file_with_env() {
3823 let mut bash = Bash::builder()
3825 .env("APP_NAME", "testapp")
3826 .mount_text("/config/app.conf", "name=${APP_NAME}")
3827 .build();
3828
3829 let result = bash.exec("echo $APP_NAME").await.unwrap();
3830 assert_eq!(result.stdout, "testapp\n");
3831
3832 let result = bash.exec("cat /config/app.conf").await.unwrap();
3833 assert_eq!(result.stdout, "name=${APP_NAME}");
3834 }
3835
3836 #[tokio::test]
3837 async fn test_text_file_json() {
3838 let mut bash = Bash::builder()
3839 .mount_text("/data/users.json", r#"["alice", "bob", "charlie"]"#)
3840 .build();
3841
3842 let result = bash.exec("cat /data/users.json | jq '.[0]'").await.unwrap();
3843 assert_eq!(result.stdout, "\"alice\"\n");
3844 }
3845
3846 #[tokio::test]
3847 async fn test_mount_with_custom_filesystem() {
3848 let custom_fs = std::sync::Arc::new(InMemoryFs::new());
3850
3851 custom_fs
3853 .write_file(std::path::Path::new("/base.txt"), b"from base")
3854 .await
3855 .unwrap();
3856
3857 let mut bash = Bash::builder()
3858 .fs(custom_fs)
3859 .mount_text("/mounted.txt", "from mount")
3860 .mount_readonly_text("/readonly.txt", "immutable")
3861 .build();
3862
3863 let result = bash.exec("cat /base.txt").await.unwrap();
3865 assert_eq!(result.stdout, "from base");
3866
3867 let result = bash.exec("cat /mounted.txt").await.unwrap();
3869 assert_eq!(result.stdout, "from mount");
3870
3871 let result = bash.exec("cat /readonly.txt").await.unwrap();
3872 assert_eq!(result.stdout, "immutable");
3873
3874 let stat = bash
3876 .fs()
3877 .stat(std::path::Path::new("/readonly.txt"))
3878 .await
3879 .unwrap();
3880 assert_eq!(stat.mode, 0o444);
3881 }
3882
3883 #[tokio::test]
3884 async fn test_mount_overwrites_base_file() {
3885 let custom_fs = std::sync::Arc::new(InMemoryFs::new());
3887 custom_fs
3888 .write_file(std::path::Path::new("/config.txt"), b"original")
3889 .await
3890 .unwrap();
3891
3892 let mut bash = Bash::builder()
3893 .fs(custom_fs)
3894 .mount_text("/config.txt", "overwritten")
3895 .build();
3896
3897 let result = bash.exec("cat /config.txt").await.unwrap();
3898 assert_eq!(result.stdout, "overwritten");
3899 }
3900
3901 #[tokio::test]
3906 async fn test_parse_error_includes_line_number() {
3907 let mut bash = Bash::new();
3909 let result = bash
3910 .exec(
3911 r#"echo ok
3912if true; then
3913echo missing fi"#,
3914 )
3915 .await;
3916 assert!(result.is_err());
3918 let err = result.unwrap_err();
3919 let err_msg = format!("{}", err);
3920 assert!(
3922 err_msg.contains("line") || err_msg.contains("parse"),
3923 "Error should be a parse error: {}",
3924 err_msg
3925 );
3926 }
3927
3928 #[tokio::test]
3929 async fn test_parse_error_on_specific_line() {
3930 use crate::parser::Parser;
3932 let script = "echo line1\necho line2\nif true; then\n";
3933 let result = Parser::new(script).parse();
3934 assert!(result.is_err());
3935 let err = result.unwrap_err();
3936 let err_msg = format!("{}", err);
3937 assert!(
3939 err_msg.contains("expected") || err_msg.contains("syntax error"),
3940 "Error should be a parse error: {}",
3941 err_msg
3942 );
3943 }
3944
3945 #[tokio::test]
3948 async fn test_cd_to_root_and_ls() {
3949 let mut bash = Bash::new();
3951 let result = bash.exec("cd / && ls").await.unwrap();
3952 assert_eq!(
3953 result.exit_code, 0,
3954 "cd / && ls should succeed: {}",
3955 result.stderr
3956 );
3957 assert!(result.stdout.contains("tmp"), "Root should contain tmp");
3958 assert!(result.stdout.contains("home"), "Root should contain home");
3959 }
3960
3961 #[tokio::test]
3962 async fn test_cd_to_root_and_pwd() {
3963 let mut bash = Bash::new();
3965 let result = bash.exec("cd / && pwd").await.unwrap();
3966 assert_eq!(result.exit_code, 0, "cd / && pwd should succeed");
3967 assert_eq!(result.stdout.trim(), "/");
3968 }
3969
3970 #[tokio::test]
3971 async fn test_cd_to_root_and_ls_dot() {
3972 let mut bash = Bash::new();
3974 let result = bash.exec("cd / && ls .").await.unwrap();
3975 assert_eq!(
3976 result.exit_code, 0,
3977 "cd / && ls . should succeed: {}",
3978 result.stderr
3979 );
3980 assert!(result.stdout.contains("tmp"), "Root should contain tmp");
3981 assert!(result.stdout.contains("home"), "Root should contain home");
3982 }
3983
3984 #[tokio::test]
3985 async fn test_ls_root_directly() {
3986 let mut bash = Bash::new();
3988 let result = bash.exec("ls /").await.unwrap();
3989 assert_eq!(
3990 result.exit_code, 0,
3991 "ls / should succeed: {}",
3992 result.stderr
3993 );
3994 assert!(result.stdout.contains("tmp"), "Root should contain tmp");
3995 assert!(result.stdout.contains("home"), "Root should contain home");
3996 assert!(result.stdout.contains("dev"), "Root should contain dev");
3997 }
3998
3999 #[tokio::test]
4000 async fn test_ls_root_long_format() {
4001 let mut bash = Bash::new();
4003 let result = bash.exec("ls -la /").await.unwrap();
4004 assert_eq!(
4005 result.exit_code, 0,
4006 "ls -la / should succeed: {}",
4007 result.stderr
4008 );
4009 assert!(result.stdout.contains("tmp"), "Root should contain tmp");
4010 assert!(
4011 result.stdout.contains("drw"),
4012 "Should show directory permissions"
4013 );
4014 }
4015
4016 #[tokio::test]
4019 async fn test_heredoc_redirect_to_file() {
4020 let mut bash = Bash::new();
4022 let result = bash
4023 .exec("cat > /tmp/out.txt <<'EOF'\nhello\nworld\nEOF\ncat /tmp/out.txt")
4024 .await
4025 .unwrap();
4026 assert_eq!(result.stdout, "hello\nworld\n");
4027 assert_eq!(result.exit_code, 0);
4028 }
4029
4030 #[tokio::test]
4031 async fn test_heredoc_redirect_to_file_unquoted() {
4032 let mut bash = Bash::new();
4033 let result = bash
4034 .exec("cat > /tmp/out.txt <<EOF\nhello\nworld\nEOF\ncat /tmp/out.txt")
4035 .await
4036 .unwrap();
4037 assert_eq!(result.stdout, "hello\nworld\n");
4038 assert_eq!(result.exit_code, 0);
4039 }
4040
4041 #[tokio::test]
4044 async fn test_pipe_to_while_read() {
4045 let mut bash = Bash::new();
4047 let result = bash
4048 .exec("echo -e 'a\\nb\\nc' | while read line; do echo \"got: $line\"; done")
4049 .await
4050 .unwrap();
4051 assert!(
4052 result.stdout.contains("got: a"),
4053 "stdout: {}",
4054 result.stdout
4055 );
4056 assert!(
4057 result.stdout.contains("got: b"),
4058 "stdout: {}",
4059 result.stdout
4060 );
4061 assert!(
4062 result.stdout.contains("got: c"),
4063 "stdout: {}",
4064 result.stdout
4065 );
4066 }
4067
4068 #[tokio::test]
4069 async fn test_pipe_to_while_read_count() {
4070 let mut bash = Bash::new();
4071 let result = bash
4072 .exec("printf 'x\\ny\\nz\\n' | while read line; do echo $line; done")
4073 .await
4074 .unwrap();
4075 assert_eq!(result.stdout, "x\ny\nz\n");
4076 }
4077
4078 #[tokio::test]
4081 async fn test_source_loads_functions() {
4082 let mut bash = Bash::new();
4083 bash.exec("cat > /tmp/lib.sh <<'EOF'\ngreet() { echo \"hello $1\"; }\nEOF")
4085 .await
4086 .unwrap();
4087 let result = bash.exec("source /tmp/lib.sh; greet world").await.unwrap();
4088 assert_eq!(result.stdout, "hello world\n");
4089 assert_eq!(result.exit_code, 0);
4090 }
4091
4092 #[tokio::test]
4093 async fn test_source_loads_variables() {
4094 let mut bash = Bash::new();
4095 bash.exec("echo 'MY_VAR=loaded' > /tmp/vars.sh")
4096 .await
4097 .unwrap();
4098 let result = bash
4099 .exec("source /tmp/vars.sh; echo $MY_VAR")
4100 .await
4101 .unwrap();
4102 assert_eq!(result.stdout, "loaded\n");
4103 }
4104
4105 #[tokio::test]
4108 async fn test_chmod_symbolic_plus_x() {
4109 let mut bash = Bash::new();
4110 bash.exec("echo '#!/bin/bash' > /tmp/script.sh")
4111 .await
4112 .unwrap();
4113 let result = bash.exec("chmod +x /tmp/script.sh").await.unwrap();
4114 assert_eq!(
4115 result.exit_code, 0,
4116 "chmod +x should succeed: {}",
4117 result.stderr
4118 );
4119 }
4120
4121 #[tokio::test]
4122 async fn test_chmod_symbolic_u_plus_x() {
4123 let mut bash = Bash::new();
4124 bash.exec("echo 'test' > /tmp/file.txt").await.unwrap();
4125 let result = bash.exec("chmod u+x /tmp/file.txt").await.unwrap();
4126 assert_eq!(
4127 result.exit_code, 0,
4128 "chmod u+x should succeed: {}",
4129 result.stderr
4130 );
4131 }
4132
4133 #[tokio::test]
4134 async fn test_chmod_symbolic_a_plus_r() {
4135 let mut bash = Bash::new();
4136 bash.exec("echo 'test' > /tmp/file.txt").await.unwrap();
4137 let result = bash.exec("chmod a+r /tmp/file.txt").await.unwrap();
4138 assert_eq!(
4139 result.exit_code, 0,
4140 "chmod a+r should succeed: {}",
4141 result.stderr
4142 );
4143 }
4144
4145 #[tokio::test]
4148 async fn test_awk_array_length() {
4149 let mut bash = Bash::new();
4151 let result = bash
4152 .exec(r#"echo "" | awk 'BEGIN{a[1]="x"; a[2]="y"; a[3]="z"} END{print length(a)}'"#)
4153 .await
4154 .unwrap();
4155 assert_eq!(result.stdout, "3\n");
4156 }
4157
4158 #[tokio::test]
4159 async fn test_awk_array_read_after_split() {
4160 let mut bash = Bash::new();
4162 let result = bash
4163 .exec(r#"echo "a:b:c" | awk '{n=split($0,arr,":"); for(i=1;i<=n;i++) print arr[i]}'"#)
4164 .await
4165 .unwrap();
4166 assert_eq!(result.stdout, "a\nb\nc\n");
4167 }
4168
4169 #[tokio::test]
4170 async fn test_awk_array_word_count_pattern() {
4171 let mut bash = Bash::new();
4173 let result = bash
4174 .exec(
4175 r#"printf "apple\nbanana\napple\ncherry\nbanana\napple" | awk '{count[$1]++} END{for(w in count) print w, count[w]}'"#,
4176 )
4177 .await
4178 .unwrap();
4179 assert!(
4180 result.stdout.contains("apple 3"),
4181 "stdout: {}",
4182 result.stdout
4183 );
4184 assert!(
4185 result.stdout.contains("banana 2"),
4186 "stdout: {}",
4187 result.stdout
4188 );
4189 assert!(
4190 result.stdout.contains("cherry 1"),
4191 "stdout: {}",
4192 result.stdout
4193 );
4194 }
4195
4196 #[tokio::test]
4199 async fn test_exec_streaming_for_loop() {
4200 let chunks = Arc::new(Mutex::new(Vec::new()));
4201 let chunks_cb = chunks.clone();
4202 let mut bash = Bash::new();
4203
4204 let result = bash
4205 .exec_streaming(
4206 "for i in 1 2 3; do echo $i; done",
4207 Box::new(move |stdout, _stderr| {
4208 chunks_cb.lock().unwrap().push(stdout.to_string());
4209 }),
4210 )
4211 .await
4212 .unwrap();
4213
4214 assert_eq!(result.stdout, "1\n2\n3\n");
4215 assert_eq!(
4216 *chunks.lock().unwrap(),
4217 vec!["1\n", "2\n", "3\n"],
4218 "each loop iteration should stream separately"
4219 );
4220 }
4221
4222 #[tokio::test]
4223 async fn test_exec_streaming_while_loop() {
4224 let chunks = Arc::new(Mutex::new(Vec::new()));
4225 let chunks_cb = chunks.clone();
4226 let mut bash = Bash::new();
4227
4228 let result = bash
4229 .exec_streaming(
4230 "i=0; while [ $i -lt 3 ]; do i=$((i+1)); echo $i; done",
4231 Box::new(move |stdout, _stderr| {
4232 chunks_cb.lock().unwrap().push(stdout.to_string());
4233 }),
4234 )
4235 .await
4236 .unwrap();
4237
4238 assert_eq!(result.stdout, "1\n2\n3\n");
4239 let chunks = chunks.lock().unwrap();
4240 assert!(
4242 chunks.contains(&"1\n".to_string()),
4243 "should contain first iteration output"
4244 );
4245 assert!(
4246 chunks.contains(&"2\n".to_string()),
4247 "should contain second iteration output"
4248 );
4249 assert!(
4250 chunks.contains(&"3\n".to_string()),
4251 "should contain third iteration output"
4252 );
4253 }
4254
4255 #[tokio::test]
4256 async fn test_exec_streaming_no_callback_still_works() {
4257 let mut bash = Bash::new();
4259 let result = bash.exec("for i in a b c; do echo $i; done").await.unwrap();
4260 assert_eq!(result.stdout, "a\nb\nc\n");
4261 }
4262
4263 #[tokio::test]
4264 async fn test_exec_streaming_nested_loops_no_duplicates() {
4265 let chunks = Arc::new(Mutex::new(Vec::new()));
4266 let chunks_cb = chunks.clone();
4267 let mut bash = Bash::new();
4268
4269 let result = bash
4270 .exec_streaming(
4271 "for i in 1 2; do for j in a b; do echo \"$i$j\"; done; done",
4272 Box::new(move |stdout, _stderr| {
4273 chunks_cb.lock().unwrap().push(stdout.to_string());
4274 }),
4275 )
4276 .await
4277 .unwrap();
4278
4279 assert_eq!(result.stdout, "1a\n1b\n2a\n2b\n");
4280 let chunks = chunks.lock().unwrap();
4281 let total_chars: usize = chunks.iter().map(|c| c.len()).sum();
4283 assert_eq!(
4284 total_chars,
4285 result.stdout.len(),
4286 "total streamed bytes should match final output: chunks={:?}",
4287 *chunks
4288 );
4289 }
4290
4291 #[tokio::test]
4292 async fn test_exec_streaming_mixed_list_and_loop() {
4293 let chunks = Arc::new(Mutex::new(Vec::new()));
4294 let chunks_cb = chunks.clone();
4295 let mut bash = Bash::new();
4296
4297 let result = bash
4298 .exec_streaming(
4299 "echo start; for i in 1 2; do echo $i; done; echo end",
4300 Box::new(move |stdout, _stderr| {
4301 chunks_cb.lock().unwrap().push(stdout.to_string());
4302 }),
4303 )
4304 .await
4305 .unwrap();
4306
4307 assert_eq!(result.stdout, "start\n1\n2\nend\n");
4308 let chunks = chunks.lock().unwrap();
4309 assert_eq!(
4310 *chunks,
4311 vec!["start\n", "1\n", "2\n", "end\n"],
4312 "mixed list+loop should produce exactly 4 events"
4313 );
4314 }
4315
4316 #[tokio::test]
4317 async fn test_exec_streaming_stderr() {
4318 let stderr_chunks = Arc::new(Mutex::new(Vec::new()));
4319 let stderr_cb = stderr_chunks.clone();
4320 let mut bash = Bash::new();
4321
4322 let result = bash
4323 .exec_streaming(
4324 "echo ok; echo err >&2; echo ok2",
4325 Box::new(move |_stdout, stderr| {
4326 if !stderr.is_empty() {
4327 stderr_cb.lock().unwrap().push(stderr.to_string());
4328 }
4329 }),
4330 )
4331 .await
4332 .unwrap();
4333
4334 assert_eq!(result.stdout, "ok\nok2\n");
4335 assert_eq!(result.stderr, "err\n");
4336 let stderr_chunks = stderr_chunks.lock().unwrap();
4337 assert!(
4338 stderr_chunks.contains(&"err\n".to_string()),
4339 "stderr should be streamed: {:?}",
4340 *stderr_chunks
4341 );
4342 }
4343
4344 async fn assert_streaming_equivalence(script: &str) {
4351 let mut bash_plain = Bash::new();
4353 let plain = bash_plain.exec(script).await.unwrap();
4354
4355 let stdout_chunks: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
4357 let stderr_chunks: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
4358 let so = stdout_chunks.clone();
4359 let se = stderr_chunks.clone();
4360 let mut bash_stream = Bash::new();
4361 let streamed = bash_stream
4362 .exec_streaming(
4363 script,
4364 Box::new(move |stdout, stderr| {
4365 if !stdout.is_empty() {
4366 so.lock().unwrap().push(stdout.to_string());
4367 }
4368 if !stderr.is_empty() {
4369 se.lock().unwrap().push(stderr.to_string());
4370 }
4371 }),
4372 )
4373 .await
4374 .unwrap();
4375
4376 assert_eq!(
4378 plain.stdout, streamed.stdout,
4379 "stdout mismatch for: {script}"
4380 );
4381 assert_eq!(
4382 plain.stderr, streamed.stderr,
4383 "stderr mismatch for: {script}"
4384 );
4385 assert_eq!(
4386 plain.exit_code, streamed.exit_code,
4387 "exit_code mismatch for: {script}"
4388 );
4389
4390 let reassembled_stdout: String = stdout_chunks.lock().unwrap().iter().cloned().collect();
4392 assert_eq!(
4393 reassembled_stdout, streamed.stdout,
4394 "reassembled stdout chunks != final stdout for: {script}"
4395 );
4396 let reassembled_stderr: String = stderr_chunks.lock().unwrap().iter().cloned().collect();
4397 assert_eq!(
4398 reassembled_stderr, streamed.stderr,
4399 "reassembled stderr chunks != final stderr for: {script}"
4400 );
4401 }
4402
4403 #[tokio::test]
4404 async fn test_streaming_equivalence_for_loop() {
4405 assert_streaming_equivalence("for i in 1 2 3; do echo $i; done").await;
4406 }
4407
4408 #[tokio::test]
4409 async fn test_streaming_equivalence_while_loop() {
4410 assert_streaming_equivalence("i=0; while [ $i -lt 4 ]; do i=$((i+1)); echo $i; done").await;
4411 }
4412
4413 #[tokio::test]
4414 async fn test_streaming_equivalence_nested_loops() {
4415 assert_streaming_equivalence("for i in a b; do for j in 1 2; do echo \"$i$j\"; done; done")
4416 .await;
4417 }
4418
4419 #[tokio::test]
4420 async fn test_streaming_equivalence_mixed_list() {
4421 assert_streaming_equivalence("echo start; for i in x y; do echo $i; done; echo end").await;
4422 }
4423
4424 #[tokio::test]
4425 async fn test_streaming_equivalence_stderr() {
4426 assert_streaming_equivalence("echo out; echo err >&2; echo out2").await;
4427 }
4428
4429 #[tokio::test]
4430 async fn test_streaming_equivalence_pipeline() {
4431 assert_streaming_equivalence("echo -e 'a\\nb\\nc' | grep b").await;
4432 }
4433
4434 #[tokio::test]
4435 async fn test_streaming_equivalence_conditionals() {
4436 assert_streaming_equivalence("if true; then echo yes; else echo no; fi; echo done").await;
4437 }
4438
4439 #[tokio::test]
4440 async fn test_streaming_equivalence_subshell() {
4441 assert_streaming_equivalence("x=$(echo hello); echo $x").await;
4442 }
4443}