1#![warn(clippy::unwrap_used)]
397#![cfg_attr(test, allow(clippy::unwrap_used))]
398
399mod builtins;
400mod error;
401mod fs;
402mod git;
403mod interpreter;
404mod limits;
405#[cfg(feature = "logging")]
406mod logging_impl;
407mod network;
408pub mod parser;
410#[cfg(feature = "scripted_tool")]
413pub mod scripted_tool;
414pub mod tool;
416pub mod trace;
418
419pub use async_trait::async_trait;
420pub use builtins::{Builtin, Context as BuiltinContext};
421pub use error::{Error, Result};
422pub use fs::{
423 DirEntry, FileSystem, FileSystemExt, FileType, FsBackend, FsLimitExceeded, FsLimits, FsUsage,
424 InMemoryFs, Metadata, MountableFs, OverlayFs, PosixFs, SearchCapabilities, SearchCapable,
425 SearchMatch, SearchProvider, SearchQuery, SearchResults, VfsSnapshot, normalize_path,
426 verify_filesystem_requirements,
427};
428#[cfg(feature = "realfs")]
429pub use fs::{RealFs, RealFsMode};
430pub use git::GitConfig;
431pub use interpreter::{ControlFlow, ExecResult, HistoryEntry, OutputCallback, ShellState};
432pub use limits::{
433 ExecutionCounters, ExecutionLimits, LimitExceeded, MemoryBudget, MemoryLimits, SessionLimits,
434};
435pub use network::NetworkAllowlist;
436pub use tool::BashToolBuilder as ToolBuilder;
437pub use tool::{
438 BashTool, BashToolBuilder, Tool, ToolError, ToolExecution, ToolImage, ToolOutput,
439 ToolOutputChunk, ToolOutputMetadata, ToolRequest, ToolResponse, ToolService, ToolStatus,
440 VERSION,
441};
442pub use trace::{
443 TraceCallback, TraceCollector, TraceEvent, TraceEventDetails, TraceEventKind, TraceMode,
444};
445
446#[cfg(feature = "scripted_tool")]
447pub use scripted_tool::{
448 DiscoverTool, DiscoveryMode, ScriptedCommandInvocation, ScriptedCommandKind,
449 ScriptedExecutionTrace, ScriptedTool, ScriptedToolBuilder, ScriptingToolSet,
450 ScriptingToolSetBuilder, ToolArgs, ToolCallback, ToolDef,
451};
452
453#[cfg(feature = "http_client")]
454pub use network::HttpClient;
455
456#[cfg(feature = "git")]
457pub use git::GitClient;
458
459#[cfg(feature = "python")]
460pub use builtins::{PythonExternalFnHandler, PythonExternalFns, PythonLimits};
461#[cfg(feature = "python")]
465pub use monty::{ExcType, ExtFunctionResult, MontyException, MontyObject};
466
467#[cfg(feature = "logging")]
472pub mod logging {
473 pub use crate::logging_impl::{LogConfig, format_script_for_log, sanitize_for_log};
474}
475
476#[cfg(feature = "logging")]
477pub use logging::LogConfig;
478
479use interpreter::Interpreter;
480use parser::Parser;
481use std::collections::HashMap;
482use std::path::PathBuf;
483use std::sync::Arc;
484
485pub struct Bash {
489 fs: Arc<dyn FileSystem>,
490 mountable: Arc<MountableFs>,
492 interpreter: Interpreter,
493 #[cfg(not(target_family = "wasm"))]
495 parser_timeout: std::time::Duration,
496 max_input_bytes: usize,
498 max_ast_depth: usize,
500 max_parser_operations: usize,
502 #[cfg(feature = "logging")]
504 log_config: logging::LogConfig,
505}
506
507impl Default for Bash {
508 fn default() -> Self {
509 Self::new()
510 }
511}
512
513impl Bash {
514 pub fn new() -> Self {
516 let base_fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
517 let mountable = Arc::new(MountableFs::new(base_fs));
518 let fs: Arc<dyn FileSystem> = Arc::clone(&mountable) as Arc<dyn FileSystem>;
519 let interpreter = Interpreter::new(Arc::clone(&fs));
520 #[cfg(not(target_family = "wasm"))]
521 let parser_timeout = ExecutionLimits::default().parser_timeout;
522 let max_input_bytes = ExecutionLimits::default().max_input_bytes;
523 let max_ast_depth = ExecutionLimits::default().max_ast_depth;
524 let max_parser_operations = ExecutionLimits::default().max_parser_operations;
525 Self {
526 fs,
527 mountable,
528 interpreter,
529 #[cfg(not(target_family = "wasm"))]
530 parser_timeout,
531 max_input_bytes,
532 max_ast_depth,
533 max_parser_operations,
534 #[cfg(feature = "logging")]
535 log_config: logging::LogConfig::default(),
536 }
537 }
538
539 pub fn builder() -> BashBuilder {
541 BashBuilder::default()
542 }
543
544 pub async fn exec(&mut self, script: &str) -> Result<ExecResult> {
550 self.interpreter.reset_transient_state();
552
553 #[cfg(feature = "logging")]
556 {
557 let script_info = logging::format_script_for_log(script, &self.log_config);
558 tracing::info!(target: "bashkit::session", script = %script_info, "Starting script execution");
559 }
560
561 let input_len = script.len();
563 if input_len > self.max_input_bytes {
564 #[cfg(feature = "logging")]
565 tracing::error!(
566 target: "bashkit::session",
567 input_len = input_len,
568 max_bytes = self.max_input_bytes,
569 "Script exceeds maximum input size"
570 );
571 return Err(Error::ResourceLimit(LimitExceeded::InputTooLarge(
572 input_len,
573 self.max_input_bytes,
574 )));
575 }
576
577 #[cfg(not(target_family = "wasm"))]
578 let parser_timeout = self.parser_timeout;
579 let max_ast_depth = self.max_ast_depth;
580 let max_parser_operations = self.max_parser_operations;
581 let script_owned = script.to_owned();
582
583 #[cfg(feature = "logging")]
584 tracing::debug!(
585 target: "bashkit::parser",
586 input_len = input_len,
587 max_ast_depth = max_ast_depth,
588 max_operations = max_parser_operations,
589 "Parsing script"
590 );
591
592 #[cfg(target_family = "wasm")]
595 let ast = {
596 let parser = Parser::with_limits(&script_owned, max_ast_depth, max_parser_operations);
597 parser.parse()?
598 };
599
600 #[cfg(not(target_family = "wasm"))]
603 let ast = {
604 let parse_result = tokio::time::timeout(parser_timeout, async {
605 tokio::task::spawn_blocking(move || {
606 let parser =
607 Parser::with_limits(&script_owned, max_ast_depth, max_parser_operations);
608 parser.parse()
609 })
610 .await
611 })
612 .await;
613
614 match parse_result {
615 Ok(Ok(result)) => {
616 match &result {
617 Ok(_) => {
618 #[cfg(feature = "logging")]
619 tracing::debug!(target: "bashkit::parser", "Parse completed successfully");
620 }
621 Err(_e) => {
622 #[cfg(feature = "logging")]
623 tracing::warn!(target: "bashkit::parser", error = %_e, "Parse error");
624 }
625 }
626 result?
627 }
628 Ok(Err(join_error)) => {
629 #[cfg(feature = "logging")]
630 tracing::error!(
631 target: "bashkit::parser",
632 error = %join_error,
633 "Parser task failed"
634 );
635 return Err(Error::parse(format!("parser task failed: {}", join_error)));
636 }
637 Err(_elapsed) => {
638 #[cfg(feature = "logging")]
639 tracing::error!(
640 target: "bashkit::parser",
641 timeout_ms = parser_timeout.as_millis() as u64,
642 "Parser timeout exceeded"
643 );
644 return Err(Error::ResourceLimit(LimitExceeded::ParserTimeout(
645 parser_timeout,
646 )));
647 }
648 }
649 };
650
651 #[cfg(feature = "logging")]
652 tracing::debug!(target: "bashkit::interpreter", "Starting interpretation");
653
654 parser::validate_budget(&ast, self.interpreter.limits())
656 .map_err(|e| Error::Execution(format!("budget validation failed: {e}")))?;
657
658 self.interpreter.load_history().await;
660
661 let exec_start = std::time::Instant::now();
662 let execution_timeout = self.interpreter.limits().timeout;
664 #[cfg(not(target_family = "wasm"))]
665 let result =
666 match tokio::time::timeout(execution_timeout, self.interpreter.execute(&ast)).await {
667 Ok(r) => r,
668 Err(_elapsed) => Err(Error::ResourceLimit(LimitExceeded::Timeout(
669 execution_timeout,
670 ))),
671 };
672 #[cfg(target_family = "wasm")]
673 let result = self.interpreter.execute(&ast).await;
674 let duration_ms = exec_start.elapsed().as_millis() as u64;
675
676 if let Ok(ref exec_result) = result {
678 let cwd = self.interpreter.cwd().to_string_lossy().to_string();
679 let timestamp = chrono::Utc::now().timestamp();
680 for line in script.lines() {
681 let trimmed = line.trim();
682 if !trimmed.is_empty() && !trimmed.starts_with('#') {
683 self.interpreter.record_history(
684 trimmed.to_string(),
685 timestamp,
686 cwd.clone(),
687 exec_result.exit_code,
688 duration_ms,
689 );
690 }
691 }
692 self.interpreter.save_history().await;
694 }
695
696 #[cfg(feature = "logging")]
697 match &result {
698 Ok(exec_result) => {
699 tracing::info!(
700 target: "bashkit::session",
701 exit_code = exec_result.exit_code,
702 stdout_len = exec_result.stdout.len(),
703 stderr_len = exec_result.stderr.len(),
704 "Script execution completed"
705 );
706 }
707 Err(e) => {
708 tracing::error!(
709 target: "bashkit::session",
710 error = %e,
711 "Script execution failed"
712 );
713 }
714 }
715
716 result
717 }
718
719 pub async fn exec_streaming(
750 &mut self,
751 script: &str,
752 output_callback: OutputCallback,
753 ) -> Result<ExecResult> {
754 self.interpreter.set_output_callback(output_callback);
755 let result = self.exec(script).await;
756 self.interpreter.clear_output_callback();
757 result
758 }
759
760 pub fn cancellation_token(&self) -> Arc<std::sync::atomic::AtomicBool> {
768 self.interpreter.cancellation_token()
769 }
770
771 pub fn fs(&self) -> Arc<dyn FileSystem> {
804 Arc::clone(&self.fs)
805 }
806
807 pub fn mount(
849 &self,
850 vfs_path: impl AsRef<std::path::Path>,
851 fs: Arc<dyn FileSystem>,
852 ) -> Result<()> {
853 self.mountable.mount(vfs_path, fs)
854 }
855
856 pub fn unmount(&self, vfs_path: impl AsRef<std::path::Path>) -> Result<()> {
889 self.mountable.unmount(vfs_path)
890 }
891
892 pub fn shell_state(&self) -> ShellState {
918 self.interpreter.shell_state()
919 }
920
921 pub fn restore_shell_state(&mut self, state: &ShellState) {
926 self.interpreter.restore_shell_state(state);
927 }
928}
929
930struct MountedFile {
967 path: PathBuf,
968 content: String,
969 mode: u32,
970}
971
972#[cfg(feature = "realfs")]
974struct MountedRealDir {
975 host_path: PathBuf,
977 vfs_mount: Option<PathBuf>,
979 mode: fs::RealFsMode,
981}
982
983#[derive(Default)]
984pub struct BashBuilder {
985 fs: Option<Arc<dyn FileSystem>>,
986 env: HashMap<String, String>,
987 cwd: Option<PathBuf>,
988 limits: ExecutionLimits,
989 session_limits: SessionLimits,
990 memory_limits: MemoryLimits,
991 trace_mode: TraceMode,
992 trace_callback: Option<TraceCallback>,
993 username: Option<String>,
994 hostname: Option<String>,
995 fixed_epoch: Option<i64>,
997 custom_builtins: HashMap<String, Box<dyn Builtin>>,
998 mounted_files: Vec<MountedFile>,
1000 #[cfg(feature = "http_client")]
1002 network_allowlist: Option<NetworkAllowlist>,
1003 #[cfg(feature = "logging")]
1005 log_config: Option<logging::LogConfig>,
1006 #[cfg(feature = "git")]
1008 git_config: Option<GitConfig>,
1009 #[cfg(feature = "realfs")]
1011 real_mounts: Vec<MountedRealDir>,
1012 history_file: Option<PathBuf>,
1014}
1015
1016impl BashBuilder {
1017 pub fn fs(mut self, fs: Arc<dyn FileSystem>) -> Self {
1019 self.fs = Some(fs);
1020 self
1021 }
1022
1023 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1025 self.env.insert(key.into(), value.into());
1026 self
1027 }
1028
1029 pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
1031 self.cwd = Some(cwd.into());
1032 self
1033 }
1034
1035 pub fn limits(mut self, limits: ExecutionLimits) -> Self {
1037 self.limits = limits;
1038 self
1039 }
1040
1041 pub fn session_limits(mut self, limits: SessionLimits) -> Self {
1046 self.session_limits = limits;
1047 self
1048 }
1049
1050 pub fn memory_limits(mut self, limits: MemoryLimits) -> Self {
1055 self.memory_limits = limits;
1056 self
1057 }
1058
1059 pub fn trace_mode(mut self, mode: TraceMode) -> Self {
1065 self.trace_mode = mode;
1066 self
1067 }
1068
1069 pub fn on_trace_event(mut self, callback: TraceCallback) -> Self {
1074 self.trace_callback = Some(callback);
1075 self
1076 }
1077
1078 pub fn username(mut self, username: impl Into<String>) -> Self {
1083 self.username = Some(username.into());
1084 self
1085 }
1086
1087 pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
1091 self.hostname = Some(hostname.into());
1092 self
1093 }
1094
1095 pub fn tty(mut self, fd: u32, is_terminal: bool) -> Self {
1109 if is_terminal {
1110 self.env.insert(format!("_TTY_{}", fd), "1".to_string());
1111 }
1112 self
1113 }
1114
1115 pub fn fixed_epoch(mut self, epoch: i64) -> Self {
1120 self.fixed_epoch = Some(epoch);
1121 self
1122 }
1123
1124 pub fn history_file(mut self, path: impl Into<PathBuf>) -> Self {
1129 self.history_file = Some(path.into());
1130 self
1131 }
1132
1133 #[cfg(feature = "http_client")]
1165 pub fn network(mut self, allowlist: NetworkAllowlist) -> Self {
1166 self.network_allowlist = Some(allowlist);
1167 self
1168 }
1169
1170 #[cfg(feature = "logging")]
1207 pub fn log_config(mut self, config: logging::LogConfig) -> Self {
1208 self.log_config = Some(config);
1209 self
1210 }
1211
1212 #[cfg(feature = "git")]
1241 pub fn git(mut self, config: GitConfig) -> Self {
1242 self.git_config = Some(config);
1243 self
1244 }
1245
1246 #[cfg(feature = "python")]
1261 pub fn python(self) -> Self {
1262 self.python_with_limits(builtins::PythonLimits::default())
1263 }
1264
1265 #[cfg(feature = "python")]
1280 pub fn python_with_limits(self, limits: builtins::PythonLimits) -> Self {
1281 self.builtin(
1282 "python",
1283 Box::new(builtins::Python::with_limits(limits.clone())),
1284 )
1285 .builtin("python3", Box::new(builtins::Python::with_limits(limits)))
1286 }
1287
1288 #[cfg(feature = "python")]
1292 pub fn python_with_external_handler(
1293 self,
1294 limits: builtins::PythonLimits,
1295 external_fns: Vec<String>,
1296 handler: builtins::PythonExternalFnHandler,
1297 ) -> Self {
1298 self.builtin(
1299 "python",
1300 Box::new(
1301 builtins::Python::with_limits(limits.clone())
1302 .with_external_handler(external_fns.clone(), handler.clone()),
1303 ),
1304 )
1305 .builtin(
1306 "python3",
1307 Box::new(
1308 builtins::Python::with_limits(limits).with_external_handler(external_fns, handler),
1309 ),
1310 )
1311 }
1312
1313 pub fn builtin(mut self, name: impl Into<String>, builtin: Box<dyn Builtin>) -> Self {
1350 self.custom_builtins.insert(name.into(), builtin);
1351 self
1352 }
1353
1354 pub fn mount_text(mut self, path: impl Into<PathBuf>, content: impl Into<String>) -> Self {
1383 self.mounted_files.push(MountedFile {
1384 path: path.into(),
1385 content: content.into(),
1386 mode: 0o644,
1387 });
1388 self
1389 }
1390
1391 pub fn mount_readonly_text(
1430 mut self,
1431 path: impl Into<PathBuf>,
1432 content: impl Into<String>,
1433 ) -> Self {
1434 self.mounted_files.push(MountedFile {
1435 path: path.into(),
1436 content: content.into(),
1437 mode: 0o444,
1438 });
1439 self
1440 }
1441
1442 #[cfg(feature = "realfs")]
1460 pub fn mount_real_readonly(mut self, host_path: impl Into<PathBuf>) -> Self {
1461 self.real_mounts.push(MountedRealDir {
1462 host_path: host_path.into(),
1463 vfs_mount: None,
1464 mode: fs::RealFsMode::ReadOnly,
1465 });
1466 self
1467 }
1468
1469 #[cfg(feature = "realfs")]
1487 pub fn mount_real_readonly_at(
1488 mut self,
1489 host_path: impl Into<PathBuf>,
1490 vfs_mount: impl Into<PathBuf>,
1491 ) -> Self {
1492 self.real_mounts.push(MountedRealDir {
1493 host_path: host_path.into(),
1494 vfs_mount: Some(vfs_mount.into()),
1495 mode: fs::RealFsMode::ReadOnly,
1496 });
1497 self
1498 }
1499
1500 #[cfg(feature = "realfs")]
1517 pub fn mount_real_readwrite(mut self, host_path: impl Into<PathBuf>) -> Self {
1518 self.real_mounts.push(MountedRealDir {
1519 host_path: host_path.into(),
1520 vfs_mount: None,
1521 mode: fs::RealFsMode::ReadWrite,
1522 });
1523 self
1524 }
1525
1526 #[cfg(feature = "realfs")]
1541 pub fn mount_real_readwrite_at(
1542 mut self,
1543 host_path: impl Into<PathBuf>,
1544 vfs_mount: impl Into<PathBuf>,
1545 ) -> Self {
1546 self.real_mounts.push(MountedRealDir {
1547 host_path: host_path.into(),
1548 vfs_mount: Some(vfs_mount.into()),
1549 mode: fs::RealFsMode::ReadWrite,
1550 });
1551 self
1552 }
1553
1554 pub fn build(self) -> Bash {
1589 let base_fs = self.fs.unwrap_or_else(|| Arc::new(InMemoryFs::new()));
1590
1591 #[cfg(feature = "realfs")]
1593 let base_fs = Self::apply_real_mounts(&self.real_mounts, base_fs);
1594
1595 let base_fs: Arc<dyn FileSystem> = if self.mounted_files.is_empty() {
1597 base_fs
1598 } else {
1599 let overlay = OverlayFs::new(base_fs);
1600 for mf in &self.mounted_files {
1602 overlay.upper().add_file(&mf.path, &mf.content, mf.mode);
1603 }
1604 Arc::new(overlay)
1605 };
1606
1607 let mountable = Arc::new(MountableFs::new(base_fs));
1609 let fs: Arc<dyn FileSystem> = Arc::clone(&mountable) as Arc<dyn FileSystem>;
1610
1611 Self::build_with_fs(
1612 fs,
1613 mountable,
1614 self.env,
1615 self.username,
1616 self.hostname,
1617 self.fixed_epoch,
1618 self.cwd,
1619 self.limits,
1620 self.session_limits,
1621 self.memory_limits,
1622 self.trace_mode,
1623 self.trace_callback,
1624 self.custom_builtins,
1625 self.history_file,
1626 #[cfg(feature = "http_client")]
1627 self.network_allowlist,
1628 #[cfg(feature = "logging")]
1629 self.log_config,
1630 #[cfg(feature = "git")]
1631 self.git_config,
1632 )
1633 }
1634
1635 #[cfg(feature = "realfs")]
1640 fn apply_real_mounts(
1641 real_mounts: &[MountedRealDir],
1642 base_fs: Arc<dyn FileSystem>,
1643 ) -> Arc<dyn FileSystem> {
1644 if real_mounts.is_empty() {
1645 return base_fs;
1646 }
1647
1648 let mut current_fs = base_fs;
1649 let mut mount_points: Vec<(PathBuf, Arc<dyn FileSystem>)> = Vec::new();
1650
1651 for m in real_mounts {
1652 let real_backend = match fs::RealFs::new(&m.host_path, m.mode) {
1653 Ok(b) => b,
1654 Err(e) => {
1655 eprintln!(
1656 "bashkit: warning: failed to mount {}: {}",
1657 m.host_path.display(),
1658 e
1659 );
1660 continue;
1661 }
1662 };
1663 let real_fs: Arc<dyn FileSystem> = Arc::new(PosixFs::new(real_backend));
1664
1665 match &m.vfs_mount {
1666 None => {
1667 current_fs = Arc::new(OverlayFs::new(real_fs));
1670 }
1671 Some(mount_point) => {
1672 mount_points.push((mount_point.clone(), real_fs));
1673 }
1674 }
1675 }
1676
1677 if !mount_points.is_empty() {
1679 let mountable = MountableFs::new(current_fs);
1680 for (path, fs) in mount_points {
1681 if let Err(e) = mountable.mount(&path, fs) {
1682 eprintln!(
1683 "bashkit: warning: failed to mount at {}: {}",
1684 path.display(),
1685 e
1686 );
1687 }
1688 }
1689 Arc::new(mountable)
1690 } else {
1691 current_fs
1692 }
1693 }
1694
1695 #[allow(clippy::too_many_arguments)]
1697 fn build_with_fs(
1698 fs: Arc<dyn FileSystem>,
1699 mountable: Arc<MountableFs>,
1700 env: HashMap<String, String>,
1701 username: Option<String>,
1702 hostname: Option<String>,
1703 fixed_epoch: Option<i64>,
1704 cwd: Option<PathBuf>,
1705 limits: ExecutionLimits,
1706 session_limits: SessionLimits,
1707 memory_limits: MemoryLimits,
1708 trace_mode: TraceMode,
1709 trace_callback: Option<TraceCallback>,
1710 custom_builtins: HashMap<String, Box<dyn Builtin>>,
1711 history_file: Option<PathBuf>,
1712 #[cfg(feature = "http_client")] network_allowlist: Option<NetworkAllowlist>,
1713 #[cfg(feature = "logging")] log_config: Option<logging::LogConfig>,
1714 #[cfg(feature = "git")] git_config: Option<GitConfig>,
1715 ) -> Bash {
1716 #[cfg(feature = "logging")]
1717 let log_config = log_config.unwrap_or_default();
1718
1719 #[cfg(feature = "logging")]
1720 tracing::debug!(
1721 target: "bashkit::config",
1722 redact_sensitive = log_config.redact_sensitive,
1723 log_scripts = log_config.log_script_content,
1724 "Bash instance configured"
1725 );
1726
1727 let mut interpreter = Interpreter::with_config(
1728 Arc::clone(&fs),
1729 username.clone(),
1730 hostname,
1731 fixed_epoch,
1732 custom_builtins,
1733 );
1734
1735 for (key, value) in &env {
1737 interpreter.set_env(key, value);
1738 interpreter.set_var(key, value);
1741 }
1742 drop(env);
1743
1744 if let Some(ref username) = username {
1746 interpreter.set_env("USER", username);
1747 interpreter.set_var("USER", username);
1748 }
1749
1750 if let Some(cwd) = cwd {
1751 interpreter.set_cwd(cwd);
1752 }
1753
1754 #[cfg(feature = "http_client")]
1756 if let Some(allowlist) = network_allowlist {
1757 let client = network::HttpClient::new(allowlist);
1758 interpreter.set_http_client(client);
1759 }
1760
1761 #[cfg(feature = "git")]
1763 if let Some(config) = git_config {
1764 let client = git::GitClient::new(config);
1765 interpreter.set_git_client(client);
1766 }
1767
1768 if let Some(hf) = history_file {
1770 interpreter.set_history_file(hf);
1771 }
1772
1773 #[cfg(not(target_family = "wasm"))]
1774 let parser_timeout = limits.parser_timeout;
1775 let max_input_bytes = limits.max_input_bytes;
1776 let max_ast_depth = limits.max_ast_depth;
1777 let max_parser_operations = limits.max_parser_operations;
1778 interpreter.set_limits(limits);
1779 interpreter.set_session_limits(session_limits);
1780 interpreter.set_memory_limits(memory_limits);
1781 let mut trace_collector = TraceCollector::new(trace_mode);
1782 if let Some(cb) = trace_callback {
1783 trace_collector.set_callback(cb);
1784 }
1785 interpreter.set_trace(trace_collector);
1786
1787 Bash {
1788 fs,
1789 mountable,
1790 interpreter,
1791 #[cfg(not(target_family = "wasm"))]
1792 parser_timeout,
1793 max_input_bytes,
1794 max_ast_depth,
1795 max_parser_operations,
1796 #[cfg(feature = "logging")]
1797 log_config,
1798 }
1799 }
1800}
1801
1802#[doc = include_str!("../docs/custom_builtins.md")]
1819pub mod custom_builtins_guide {}
1820
1821#[doc = include_str!("../docs/compatibility.md")]
1831pub mod compatibility_scorecard {}
1832
1833#[doc = include_str!("../docs/threat-model.md")]
1847pub mod threat_model {}
1848
1849#[cfg(feature = "python")]
1863#[doc = include_str!("../docs/python.md")]
1864pub mod python_guide {}
1865
1866#[doc = include_str!("../docs/live_mounts.md")]
1876pub mod live_mounts_guide {}
1877
1878#[cfg(feature = "logging")]
1891#[doc = include_str!("../docs/logging.md")]
1892pub mod logging_guide {}
1893
1894#[cfg(test)]
1895mod tests {
1896 use super::*;
1897 use std::sync::{Arc, Mutex};
1898
1899 #[tokio::test]
1900 async fn test_echo_hello() {
1901 let mut bash = Bash::new();
1902 let result = bash.exec("echo hello").await.unwrap();
1903 assert_eq!(result.stdout, "hello\n");
1904 assert_eq!(result.exit_code, 0);
1905 }
1906
1907 #[tokio::test]
1908 async fn test_echo_multiple_args() {
1909 let mut bash = Bash::new();
1910 let result = bash.exec("echo hello world").await.unwrap();
1911 assert_eq!(result.stdout, "hello world\n");
1912 assert_eq!(result.exit_code, 0);
1913 }
1914
1915 #[tokio::test]
1916 async fn test_variable_expansion() {
1917 let mut bash = Bash::builder().env("HOME", "/home/user").build();
1918 let result = bash.exec("echo $HOME").await.unwrap();
1919 assert_eq!(result.stdout, "/home/user\n");
1920 assert_eq!(result.exit_code, 0);
1921 }
1922
1923 #[tokio::test]
1924 async fn test_variable_brace_expansion() {
1925 let mut bash = Bash::builder().env("USER", "testuser").build();
1926 let result = bash.exec("echo ${USER}").await.unwrap();
1927 assert_eq!(result.stdout, "testuser\n");
1928 }
1929
1930 #[tokio::test]
1931 async fn test_undefined_variable_expands_to_empty() {
1932 let mut bash = Bash::new();
1933 let result = bash.exec("echo $UNDEFINED_VAR").await.unwrap();
1934 assert_eq!(result.stdout, "\n");
1935 }
1936
1937 #[tokio::test]
1938 async fn test_pipeline() {
1939 let mut bash = Bash::new();
1940 let result = bash.exec("echo hello | cat").await.unwrap();
1941 assert_eq!(result.stdout, "hello\n");
1942 }
1943
1944 #[tokio::test]
1945 async fn test_pipeline_three_commands() {
1946 let mut bash = Bash::new();
1947 let result = bash.exec("echo hello | cat | cat").await.unwrap();
1948 assert_eq!(result.stdout, "hello\n");
1949 }
1950
1951 #[tokio::test]
1952 async fn test_redirect_output() {
1953 let mut bash = Bash::new();
1954 let result = bash.exec("echo hello > /tmp/test.txt").await.unwrap();
1955 assert_eq!(result.stdout, "");
1956 assert_eq!(result.exit_code, 0);
1957
1958 let result = bash.exec("cat /tmp/test.txt").await.unwrap();
1960 assert_eq!(result.stdout, "hello\n");
1961 }
1962
1963 #[tokio::test]
1964 async fn test_redirect_append() {
1965 let mut bash = Bash::new();
1966 bash.exec("echo hello > /tmp/append.txt").await.unwrap();
1967 bash.exec("echo world >> /tmp/append.txt").await.unwrap();
1968
1969 let result = bash.exec("cat /tmp/append.txt").await.unwrap();
1970 assert_eq!(result.stdout, "hello\nworld\n");
1971 }
1972
1973 #[tokio::test]
1974 async fn test_command_list_and() {
1975 let mut bash = Bash::new();
1976 let result = bash.exec("true && echo success").await.unwrap();
1977 assert_eq!(result.stdout, "success\n");
1978 }
1979
1980 #[tokio::test]
1981 async fn test_command_list_and_short_circuit() {
1982 let mut bash = Bash::new();
1983 let result = bash.exec("false && echo should_not_print").await.unwrap();
1984 assert_eq!(result.stdout, "");
1985 assert_eq!(result.exit_code, 1);
1986 }
1987
1988 #[tokio::test]
1989 async fn test_command_list_or() {
1990 let mut bash = Bash::new();
1991 let result = bash.exec("false || echo fallback").await.unwrap();
1992 assert_eq!(result.stdout, "fallback\n");
1993 }
1994
1995 #[tokio::test]
1996 async fn test_command_list_or_short_circuit() {
1997 let mut bash = Bash::new();
1998 let result = bash.exec("true || echo should_not_print").await.unwrap();
1999 assert_eq!(result.stdout, "");
2000 assert_eq!(result.exit_code, 0);
2001 }
2002
2003 #[tokio::test]
2005 async fn test_phase1_target() {
2006 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
2007
2008 let result = bash
2009 .exec("echo $HOME | cat > /tmp/out && cat /tmp/out")
2010 .await
2011 .unwrap();
2012
2013 assert_eq!(result.stdout, "/home/testuser\n");
2014 assert_eq!(result.exit_code, 0);
2015 }
2016
2017 #[tokio::test]
2018 async fn test_redirect_input() {
2019 let mut bash = Bash::new();
2020 bash.exec("echo hello > /tmp/input.txt").await.unwrap();
2022
2023 let result = bash.exec("cat < /tmp/input.txt").await.unwrap();
2025 assert_eq!(result.stdout, "hello\n");
2026 }
2027
2028 #[tokio::test]
2029 async fn test_here_string() {
2030 let mut bash = Bash::new();
2031 let result = bash.exec("cat <<< hello").await.unwrap();
2032 assert_eq!(result.stdout, "hello\n");
2033 }
2034
2035 #[tokio::test]
2036 async fn test_if_true() {
2037 let mut bash = Bash::new();
2038 let result = bash.exec("if true; then echo yes; fi").await.unwrap();
2039 assert_eq!(result.stdout, "yes\n");
2040 }
2041
2042 #[tokio::test]
2043 async fn test_if_false() {
2044 let mut bash = Bash::new();
2045 let result = bash.exec("if false; then echo yes; fi").await.unwrap();
2046 assert_eq!(result.stdout, "");
2047 }
2048
2049 #[tokio::test]
2050 async fn test_if_else() {
2051 let mut bash = Bash::new();
2052 let result = bash
2053 .exec("if false; then echo yes; else echo no; fi")
2054 .await
2055 .unwrap();
2056 assert_eq!(result.stdout, "no\n");
2057 }
2058
2059 #[tokio::test]
2060 async fn test_if_elif() {
2061 let mut bash = Bash::new();
2062 let result = bash
2063 .exec("if false; then echo one; elif true; then echo two; else echo three; fi")
2064 .await
2065 .unwrap();
2066 assert_eq!(result.stdout, "two\n");
2067 }
2068
2069 #[tokio::test]
2070 async fn test_for_loop() {
2071 let mut bash = Bash::new();
2072 let result = bash.exec("for i in a b c; do echo $i; done").await.unwrap();
2073 assert_eq!(result.stdout, "a\nb\nc\n");
2074 }
2075
2076 #[tokio::test]
2077 async fn test_for_loop_positional_params() {
2078 let mut bash = Bash::new();
2079 let result = bash
2081 .exec("f() { for x; do echo $x; done; }; f one two three")
2082 .await
2083 .unwrap();
2084 assert_eq!(result.stdout, "one\ntwo\nthree\n");
2085 }
2086
2087 #[tokio::test]
2088 async fn test_while_loop() {
2089 let mut bash = Bash::new();
2090 let result = bash.exec("while false; do echo loop; done").await.unwrap();
2092 assert_eq!(result.stdout, "");
2093 }
2094
2095 #[tokio::test]
2096 async fn test_subshell() {
2097 let mut bash = Bash::new();
2098 let result = bash.exec("(echo hello)").await.unwrap();
2099 assert_eq!(result.stdout, "hello\n");
2100 }
2101
2102 #[tokio::test]
2103 async fn test_brace_group() {
2104 let mut bash = Bash::new();
2105 let result = bash.exec("{ echo hello; }").await.unwrap();
2106 assert_eq!(result.stdout, "hello\n");
2107 }
2108
2109 #[tokio::test]
2110 async fn test_function_keyword() {
2111 let mut bash = Bash::new();
2112 let result = bash
2113 .exec("function greet { echo hello; }; greet")
2114 .await
2115 .unwrap();
2116 assert_eq!(result.stdout, "hello\n");
2117 }
2118
2119 #[tokio::test]
2120 async fn test_function_posix() {
2121 let mut bash = Bash::new();
2122 let result = bash.exec("greet() { echo hello; }; greet").await.unwrap();
2123 assert_eq!(result.stdout, "hello\n");
2124 }
2125
2126 #[tokio::test]
2127 async fn test_function_args() {
2128 let mut bash = Bash::new();
2129 let result = bash
2130 .exec("greet() { echo $1 $2; }; greet world foo")
2131 .await
2132 .unwrap();
2133 assert_eq!(result.stdout, "world foo\n");
2134 }
2135
2136 #[tokio::test]
2137 async fn test_function_arg_count() {
2138 let mut bash = Bash::new();
2139 let result = bash
2140 .exec("count() { echo $#; }; count a b c")
2141 .await
2142 .unwrap();
2143 assert_eq!(result.stdout, "3\n");
2144 }
2145
2146 #[tokio::test]
2147 async fn test_case_literal() {
2148 let mut bash = Bash::new();
2149 let result = bash
2150 .exec("case foo in foo) echo matched ;; esac")
2151 .await
2152 .unwrap();
2153 assert_eq!(result.stdout, "matched\n");
2154 }
2155
2156 #[tokio::test]
2157 async fn test_case_wildcard() {
2158 let mut bash = Bash::new();
2159 let result = bash
2160 .exec("case bar in *) echo default ;; esac")
2161 .await
2162 .unwrap();
2163 assert_eq!(result.stdout, "default\n");
2164 }
2165
2166 #[tokio::test]
2167 async fn test_case_no_match() {
2168 let mut bash = Bash::new();
2169 let result = bash.exec("case foo in bar) echo no ;; esac").await.unwrap();
2170 assert_eq!(result.stdout, "");
2171 }
2172
2173 #[tokio::test]
2174 async fn test_case_multiple_patterns() {
2175 let mut bash = Bash::new();
2176 let result = bash
2177 .exec("case foo in bar|foo|baz) echo matched ;; esac")
2178 .await
2179 .unwrap();
2180 assert_eq!(result.stdout, "matched\n");
2181 }
2182
2183 #[tokio::test]
2184 async fn test_case_bracket_expr() {
2185 let mut bash = Bash::new();
2186 let result = bash
2188 .exec("case b in [abc]) echo matched ;; esac")
2189 .await
2190 .unwrap();
2191 assert_eq!(result.stdout, "matched\n");
2192 }
2193
2194 #[tokio::test]
2195 async fn test_case_bracket_range() {
2196 let mut bash = Bash::new();
2197 let result = bash
2199 .exec("case m in [a-z]) echo letter ;; esac")
2200 .await
2201 .unwrap();
2202 assert_eq!(result.stdout, "letter\n");
2203 }
2204
2205 #[tokio::test]
2206 async fn test_case_bracket_negation() {
2207 let mut bash = Bash::new();
2208 let result = bash
2210 .exec("case x in [!abc]) echo not_abc ;; esac")
2211 .await
2212 .unwrap();
2213 assert_eq!(result.stdout, "not_abc\n");
2214 }
2215
2216 #[tokio::test]
2217 async fn test_break_as_command() {
2218 let mut bash = Bash::new();
2219 let result = bash.exec("break").await.unwrap();
2221 assert_eq!(result.exit_code, 0);
2223 }
2224
2225 #[tokio::test]
2226 async fn test_for_one_item() {
2227 let mut bash = Bash::new();
2228 let result = bash.exec("for i in a; do echo $i; done").await.unwrap();
2230 assert_eq!(result.stdout, "a\n");
2231 }
2232
2233 #[tokio::test]
2234 async fn test_for_with_break() {
2235 let mut bash = Bash::new();
2236 let result = bash.exec("for i in a; do break; done").await.unwrap();
2238 assert_eq!(result.stdout, "");
2239 assert_eq!(result.exit_code, 0);
2240 }
2241
2242 #[tokio::test]
2243 async fn test_for_echo_break() {
2244 let mut bash = Bash::new();
2245 let result = bash
2247 .exec("for i in a b c; do echo $i; break; done")
2248 .await
2249 .unwrap();
2250 assert_eq!(result.stdout, "a\n");
2251 }
2252
2253 #[tokio::test]
2254 async fn test_test_string_empty() {
2255 let mut bash = Bash::new();
2256 let result = bash.exec("test -z '' && echo yes").await.unwrap();
2257 assert_eq!(result.stdout, "yes\n");
2258 }
2259
2260 #[tokio::test]
2261 async fn test_test_string_not_empty() {
2262 let mut bash = Bash::new();
2263 let result = bash.exec("test -n 'hello' && echo yes").await.unwrap();
2264 assert_eq!(result.stdout, "yes\n");
2265 }
2266
2267 #[tokio::test]
2268 async fn test_test_string_equal() {
2269 let mut bash = Bash::new();
2270 let result = bash.exec("test foo = foo && echo yes").await.unwrap();
2271 assert_eq!(result.stdout, "yes\n");
2272 }
2273
2274 #[tokio::test]
2275 async fn test_test_string_not_equal() {
2276 let mut bash = Bash::new();
2277 let result = bash.exec("test foo != bar && echo yes").await.unwrap();
2278 assert_eq!(result.stdout, "yes\n");
2279 }
2280
2281 #[tokio::test]
2282 async fn test_test_numeric_equal() {
2283 let mut bash = Bash::new();
2284 let result = bash.exec("test 5 -eq 5 && echo yes").await.unwrap();
2285 assert_eq!(result.stdout, "yes\n");
2286 }
2287
2288 #[tokio::test]
2289 async fn test_test_numeric_less_than() {
2290 let mut bash = Bash::new();
2291 let result = bash.exec("test 3 -lt 5 && echo yes").await.unwrap();
2292 assert_eq!(result.stdout, "yes\n");
2293 }
2294
2295 #[tokio::test]
2296 async fn test_bracket_form() {
2297 let mut bash = Bash::new();
2298 let result = bash.exec("[ foo = foo ] && echo yes").await.unwrap();
2299 assert_eq!(result.stdout, "yes\n");
2300 }
2301
2302 #[tokio::test]
2303 async fn test_if_with_test() {
2304 let mut bash = Bash::new();
2305 let result = bash
2306 .exec("if [ 5 -gt 3 ]; then echo bigger; fi")
2307 .await
2308 .unwrap();
2309 assert_eq!(result.stdout, "bigger\n");
2310 }
2311
2312 #[tokio::test]
2313 async fn test_variable_assignment() {
2314 let mut bash = Bash::new();
2315 let result = bash.exec("FOO=bar; echo $FOO").await.unwrap();
2316 assert_eq!(result.stdout, "bar\n");
2317 }
2318
2319 #[tokio::test]
2320 async fn test_variable_assignment_inline() {
2321 let mut bash = Bash::new();
2322 let result = bash.exec("MSG=hello; echo $MSG world").await.unwrap();
2324 assert_eq!(result.stdout, "hello world\n");
2325 }
2326
2327 #[tokio::test]
2328 async fn test_variable_assignment_only() {
2329 let mut bash = Bash::new();
2330 let result = bash.exec("FOO=bar").await.unwrap();
2332 assert_eq!(result.stdout, "");
2333 assert_eq!(result.exit_code, 0);
2334
2335 let result = bash.exec("echo $FOO").await.unwrap();
2337 assert_eq!(result.stdout, "bar\n");
2338 }
2339
2340 #[tokio::test]
2341 async fn test_multiple_assignments() {
2342 let mut bash = Bash::new();
2343 let result = bash.exec("A=1; B=2; C=3; echo $A $B $C").await.unwrap();
2344 assert_eq!(result.stdout, "1 2 3\n");
2345 }
2346
2347 #[tokio::test]
2348 async fn test_prefix_assignment_visible_in_env() {
2349 let mut bash = Bash::new();
2350 let result = bash.exec("MYVAR=hello printenv MYVAR").await.unwrap();
2352 assert_eq!(result.stdout, "hello\n");
2353 }
2354
2355 #[tokio::test]
2356 async fn test_prefix_assignment_temporary() {
2357 let mut bash = Bash::new();
2358 bash.exec("MYVAR=hello printenv MYVAR").await.unwrap();
2360 let result = bash.exec("echo ${MYVAR:-unset}").await.unwrap();
2361 assert_eq!(result.stdout, "unset\n");
2362 }
2363
2364 #[tokio::test]
2365 async fn test_prefix_assignment_does_not_clobber_existing_env() {
2366 let mut bash = Bash::new();
2367 let result = bash
2369 .exec("EXISTING=original; export EXISTING; EXISTING=temp printenv EXISTING")
2370 .await
2371 .unwrap();
2372 assert_eq!(result.stdout, "temp\n");
2373 }
2374
2375 #[tokio::test]
2376 async fn test_prefix_assignment_multiple_vars() {
2377 let mut bash = Bash::new();
2378 let result = bash.exec("A=one B=two printenv A").await.unwrap();
2380 assert_eq!(result.stdout, "one\n");
2381 assert_eq!(result.exit_code, 0);
2382 }
2383
2384 #[tokio::test]
2385 async fn test_prefix_assignment_empty_value() {
2386 let mut bash = Bash::new();
2387 let result = bash.exec("MYVAR= printenv MYVAR").await.unwrap();
2389 assert_eq!(result.stdout, "\n");
2390 assert_eq!(result.exit_code, 0);
2391 }
2392
2393 #[tokio::test]
2394 async fn test_prefix_assignment_not_found_without_prefix() {
2395 let mut bash = Bash::new();
2396 let result = bash.exec("printenv NONEXISTENT").await.unwrap();
2398 assert_eq!(result.stdout, "");
2399 assert_eq!(result.exit_code, 1);
2400 }
2401
2402 #[tokio::test]
2403 async fn test_prefix_assignment_does_not_persist_in_variables() {
2404 let mut bash = Bash::new();
2405 bash.exec("TMPVAR=gone echo ok").await.unwrap();
2407 let result = bash.exec("echo \"${TMPVAR:-unset}\"").await.unwrap();
2408 assert_eq!(result.stdout, "unset\n");
2409 }
2410
2411 #[tokio::test]
2412 async fn test_assignment_only_persists() {
2413 let mut bash = Bash::new();
2414 bash.exec("PERSIST=yes").await.unwrap();
2416 let result = bash.exec("echo $PERSIST").await.unwrap();
2417 assert_eq!(result.stdout, "yes\n");
2418 }
2419
2420 #[tokio::test]
2421 async fn test_printf_string() {
2422 let mut bash = Bash::new();
2423 let result = bash.exec("printf '%s' hello").await.unwrap();
2424 assert_eq!(result.stdout, "hello");
2425 }
2426
2427 #[tokio::test]
2428 async fn test_printf_newline() {
2429 let mut bash = Bash::new();
2430 let result = bash.exec("printf 'hello\\n'").await.unwrap();
2431 assert_eq!(result.stdout, "hello\n");
2432 }
2433
2434 #[tokio::test]
2435 async fn test_printf_multiple_args() {
2436 let mut bash = Bash::new();
2437 let result = bash.exec("printf '%s %s\\n' hello world").await.unwrap();
2438 assert_eq!(result.stdout, "hello world\n");
2439 }
2440
2441 #[tokio::test]
2442 async fn test_printf_integer() {
2443 let mut bash = Bash::new();
2444 let result = bash.exec("printf '%d' 42").await.unwrap();
2445 assert_eq!(result.stdout, "42");
2446 }
2447
2448 #[tokio::test]
2449 async fn test_export() {
2450 let mut bash = Bash::new();
2451 let result = bash.exec("export FOO=bar; echo $FOO").await.unwrap();
2452 assert_eq!(result.stdout, "bar\n");
2453 }
2454
2455 #[tokio::test]
2456 async fn test_read_basic() {
2457 let mut bash = Bash::new();
2458 let result = bash.exec("echo hello | read VAR; echo $VAR").await.unwrap();
2459 assert_eq!(result.stdout, "hello\n");
2460 }
2461
2462 #[tokio::test]
2463 async fn test_read_multiple_vars() {
2464 let mut bash = Bash::new();
2465 let result = bash
2466 .exec("echo 'a b c' | read X Y Z; echo $X $Y $Z")
2467 .await
2468 .unwrap();
2469 assert_eq!(result.stdout, "a b c\n");
2470 }
2471
2472 #[tokio::test]
2473 async fn test_glob_star() {
2474 let mut bash = Bash::new();
2475 bash.exec("echo a > /tmp/file1.txt").await.unwrap();
2477 bash.exec("echo b > /tmp/file2.txt").await.unwrap();
2478 bash.exec("echo c > /tmp/other.log").await.unwrap();
2479
2480 let result = bash.exec("echo /tmp/*.txt").await.unwrap();
2482 assert_eq!(result.stdout, "/tmp/file1.txt /tmp/file2.txt\n");
2483 }
2484
2485 #[tokio::test]
2486 async fn test_glob_question_mark() {
2487 let mut bash = Bash::new();
2488 bash.exec("echo a > /tmp/a1.txt").await.unwrap();
2490 bash.exec("echo b > /tmp/a2.txt").await.unwrap();
2491 bash.exec("echo c > /tmp/a10.txt").await.unwrap();
2492
2493 let result = bash.exec("echo /tmp/a?.txt").await.unwrap();
2495 assert_eq!(result.stdout, "/tmp/a1.txt /tmp/a2.txt\n");
2496 }
2497
2498 #[tokio::test]
2499 async fn test_glob_no_match() {
2500 let mut bash = Bash::new();
2501 let result = bash.exec("echo /nonexistent/*.xyz").await.unwrap();
2503 assert_eq!(result.stdout, "/nonexistent/*.xyz\n");
2504 }
2505
2506 #[tokio::test]
2507 async fn test_command_substitution() {
2508 let mut bash = Bash::new();
2509 let result = bash.exec("echo $(echo hello)").await.unwrap();
2510 assert_eq!(result.stdout, "hello\n");
2511 }
2512
2513 #[tokio::test]
2514 async fn test_command_substitution_in_string() {
2515 let mut bash = Bash::new();
2516 let result = bash.exec("echo \"result: $(echo 42)\"").await.unwrap();
2517 assert_eq!(result.stdout, "result: 42\n");
2518 }
2519
2520 #[tokio::test]
2521 async fn test_command_substitution_pipeline() {
2522 let mut bash = Bash::new();
2523 let result = bash.exec("echo $(echo hello | cat)").await.unwrap();
2524 assert_eq!(result.stdout, "hello\n");
2525 }
2526
2527 #[tokio::test]
2528 async fn test_command_substitution_variable() {
2529 let mut bash = Bash::new();
2530 let result = bash.exec("VAR=$(echo test); echo $VAR").await.unwrap();
2531 assert_eq!(result.stdout, "test\n");
2532 }
2533
2534 #[tokio::test]
2535 async fn test_arithmetic_simple() {
2536 let mut bash = Bash::new();
2537 let result = bash.exec("echo $((1 + 2))").await.unwrap();
2538 assert_eq!(result.stdout, "3\n");
2539 }
2540
2541 #[tokio::test]
2542 async fn test_arithmetic_multiply() {
2543 let mut bash = Bash::new();
2544 let result = bash.exec("echo $((3 * 4))").await.unwrap();
2545 assert_eq!(result.stdout, "12\n");
2546 }
2547
2548 #[tokio::test]
2549 async fn test_arithmetic_with_variable() {
2550 let mut bash = Bash::new();
2551 let result = bash.exec("X=5; echo $((X + 3))").await.unwrap();
2552 assert_eq!(result.stdout, "8\n");
2553 }
2554
2555 #[tokio::test]
2556 async fn test_arithmetic_complex() {
2557 let mut bash = Bash::new();
2558 let result = bash.exec("echo $((2 + 3 * 4))").await.unwrap();
2559 assert_eq!(result.stdout, "14\n");
2560 }
2561
2562 #[tokio::test]
2563 async fn test_heredoc_simple() {
2564 let mut bash = Bash::new();
2565 let result = bash.exec("cat <<EOF\nhello\nworld\nEOF").await.unwrap();
2566 assert_eq!(result.stdout, "hello\nworld\n");
2567 }
2568
2569 #[tokio::test]
2570 async fn test_heredoc_single_line() {
2571 let mut bash = Bash::new();
2572 let result = bash.exec("cat <<END\ntest\nEND").await.unwrap();
2573 assert_eq!(result.stdout, "test\n");
2574 }
2575
2576 #[tokio::test]
2577 async fn test_unset() {
2578 let mut bash = Bash::new();
2579 let result = bash
2580 .exec("FOO=bar; unset FOO; echo \"x${FOO}y\"")
2581 .await
2582 .unwrap();
2583 assert_eq!(result.stdout, "xy\n");
2584 }
2585
2586 #[tokio::test]
2587 async fn test_local_basic() {
2588 let mut bash = Bash::new();
2589 let result = bash.exec("local X=test; echo $X").await.unwrap();
2591 assert_eq!(result.stdout, "test\n");
2592 }
2593
2594 #[tokio::test]
2595 async fn test_set_option() {
2596 let mut bash = Bash::new();
2597 let result = bash.exec("set -e; echo ok").await.unwrap();
2598 assert_eq!(result.stdout, "ok\n");
2599 }
2600
2601 #[tokio::test]
2602 async fn test_param_default() {
2603 let mut bash = Bash::new();
2604 let result = bash.exec("echo ${UNSET:-default}").await.unwrap();
2606 assert_eq!(result.stdout, "default\n");
2607
2608 let result = bash.exec("X=value; echo ${X:-default}").await.unwrap();
2610 assert_eq!(result.stdout, "value\n");
2611 }
2612
2613 #[tokio::test]
2614 async fn test_param_assign_default() {
2615 let mut bash = Bash::new();
2616 let result = bash.exec("echo ${NEW:=assigned}; echo $NEW").await.unwrap();
2618 assert_eq!(result.stdout, "assigned\nassigned\n");
2619 }
2620
2621 #[tokio::test]
2622 async fn test_param_length() {
2623 let mut bash = Bash::new();
2624 let result = bash.exec("X=hello; echo ${#X}").await.unwrap();
2625 assert_eq!(result.stdout, "5\n");
2626 }
2627
2628 #[tokio::test]
2629 async fn test_param_remove_prefix() {
2630 let mut bash = Bash::new();
2631 let result = bash.exec("X=hello.world.txt; echo ${X#*.}").await.unwrap();
2633 assert_eq!(result.stdout, "world.txt\n");
2634 }
2635
2636 #[tokio::test]
2637 async fn test_param_remove_suffix() {
2638 let mut bash = Bash::new();
2639 let result = bash.exec("X=file.tar.gz; echo ${X%.*}").await.unwrap();
2641 assert_eq!(result.stdout, "file.tar\n");
2642 }
2643
2644 #[tokio::test]
2645 async fn test_array_basic() {
2646 let mut bash = Bash::new();
2647 let result = bash.exec("arr=(a b c); echo ${arr[1]}").await.unwrap();
2649 assert_eq!(result.stdout, "b\n");
2650 }
2651
2652 #[tokio::test]
2653 async fn test_array_all_elements() {
2654 let mut bash = Bash::new();
2655 let result = bash
2657 .exec("arr=(one two three); echo ${arr[@]}")
2658 .await
2659 .unwrap();
2660 assert_eq!(result.stdout, "one two three\n");
2661 }
2662
2663 #[tokio::test]
2664 async fn test_array_length() {
2665 let mut bash = Bash::new();
2666 let result = bash.exec("arr=(a b c d e); echo ${#arr[@]}").await.unwrap();
2668 assert_eq!(result.stdout, "5\n");
2669 }
2670
2671 #[tokio::test]
2672 async fn test_array_indexed_assignment() {
2673 let mut bash = Bash::new();
2674 let result = bash
2676 .exec("arr[0]=first; arr[1]=second; echo ${arr[0]} ${arr[1]}")
2677 .await
2678 .unwrap();
2679 assert_eq!(result.stdout, "first second\n");
2680 }
2681
2682 #[tokio::test]
2683 async fn test_array_single_quote_subscript_no_panic() {
2684 let mut bash = Bash::new();
2686 let _ = bash.exec("echo ${arr[\"]}").await;
2688 }
2689
2690 #[tokio::test]
2693 async fn test_command_limit() {
2694 let limits = ExecutionLimits::new().max_commands(5);
2695 let mut bash = Bash::builder().limits(limits).build();
2696
2697 let result = bash.exec("true; true; true; true; true; true").await;
2699 assert!(result.is_err());
2700 let err = result.unwrap_err();
2701 assert!(
2702 err.to_string().contains("maximum command count exceeded"),
2703 "Expected command limit error, got: {}",
2704 err
2705 );
2706 }
2707
2708 #[tokio::test]
2709 async fn test_command_limit_not_exceeded() {
2710 let limits = ExecutionLimits::new().max_commands(10);
2711 let mut bash = Bash::builder().limits(limits).build();
2712
2713 let result = bash.exec("true; true; true; true; true").await.unwrap();
2715 assert_eq!(result.exit_code, 0);
2716 }
2717
2718 #[tokio::test]
2719 async fn test_loop_iteration_limit() {
2720 let limits = ExecutionLimits::new().max_loop_iterations(5);
2721 let mut bash = Bash::builder().limits(limits).build();
2722
2723 let result = bash
2725 .exec("for i in 1 2 3 4 5 6 7 8 9 10; do echo $i; done")
2726 .await;
2727 assert!(result.is_err());
2728 let err = result.unwrap_err();
2729 assert!(
2730 err.to_string().contains("maximum loop iterations exceeded"),
2731 "Expected loop limit error, got: {}",
2732 err
2733 );
2734 }
2735
2736 #[tokio::test]
2737 async fn test_loop_iteration_limit_not_exceeded() {
2738 let limits = ExecutionLimits::new().max_loop_iterations(10);
2739 let mut bash = Bash::builder().limits(limits).build();
2740
2741 let result = bash
2743 .exec("for i in 1 2 3 4 5; do echo $i; done")
2744 .await
2745 .unwrap();
2746 assert_eq!(result.stdout, "1\n2\n3\n4\n5\n");
2747 }
2748
2749 #[tokio::test]
2750 async fn test_function_depth_limit() {
2751 let limits = ExecutionLimits::new().max_function_depth(3);
2752 let mut bash = Bash::builder().limits(limits).build();
2753
2754 let result = bash
2756 .exec("f() { echo $1; if [ $1 -lt 5 ]; then f $(($1 + 1)); fi; }; f 1")
2757 .await;
2758 assert!(result.is_err());
2759 let err = result.unwrap_err();
2760 assert!(
2761 err.to_string().contains("maximum function depth exceeded"),
2762 "Expected function depth error, got: {}",
2763 err
2764 );
2765 }
2766
2767 #[tokio::test]
2768 async fn test_function_depth_limit_not_exceeded() {
2769 let limits = ExecutionLimits::new().max_function_depth(10);
2770 let mut bash = Bash::builder().limits(limits).build();
2771
2772 let result = bash.exec("f() { echo hello; }; f").await.unwrap();
2774 assert_eq!(result.stdout, "hello\n");
2775 }
2776
2777 #[tokio::test]
2778 async fn test_while_loop_limit() {
2779 let limits = ExecutionLimits::new().max_loop_iterations(3);
2780 let mut bash = Bash::builder().limits(limits).build();
2781
2782 let result = bash
2784 .exec("i=0; while [ $i -lt 10 ]; do echo $i; i=$((i + 1)); done")
2785 .await;
2786 assert!(result.is_err());
2787 let err = result.unwrap_err();
2788 assert!(
2789 err.to_string().contains("maximum loop iterations exceeded"),
2790 "Expected loop limit error, got: {}",
2791 err
2792 );
2793 }
2794
2795 #[tokio::test]
2796 async fn test_default_limits_allow_normal_scripts() {
2797 let mut bash = Bash::new();
2799 let result = bash
2801 .exec("for i in 1 2 3 4 5; do echo $i; done && echo finished")
2802 .await
2803 .unwrap();
2804 assert_eq!(result.stdout, "1\n2\n3\n4\n5\nfinished\n");
2805 }
2806
2807 #[tokio::test]
2808 async fn test_for_followed_by_echo_done() {
2809 let mut bash = Bash::new();
2810 let result = bash
2811 .exec("for i in 1; do echo $i; done; echo ok")
2812 .await
2813 .unwrap();
2814 assert_eq!(result.stdout, "1\nok\n");
2815 }
2816
2817 #[tokio::test]
2820 async fn test_fs_read_write_binary() {
2821 let bash = Bash::new();
2822 let fs = bash.fs();
2823 let path = std::path::Path::new("/tmp/binary.bin");
2824
2825 let binary_data: Vec<u8> = vec![0x00, 0x01, 0xFF, 0xFE, 0x42, 0x00, 0x7F];
2827 fs.write_file(path, &binary_data).await.unwrap();
2828
2829 let content = fs.read_file(path).await.unwrap();
2831 assert_eq!(content, binary_data);
2832 }
2833
2834 #[tokio::test]
2835 async fn test_fs_write_then_exec_cat() {
2836 let mut bash = Bash::new();
2837 let path = std::path::Path::new("/tmp/prepopulated.txt");
2838
2839 bash.fs()
2841 .write_file(path, b"Hello from Rust!\n")
2842 .await
2843 .unwrap();
2844
2845 let result = bash.exec("cat /tmp/prepopulated.txt").await.unwrap();
2847 assert_eq!(result.stdout, "Hello from Rust!\n");
2848 }
2849
2850 #[tokio::test]
2851 async fn test_fs_exec_then_read() {
2852 let mut bash = Bash::new();
2853 let path = std::path::Path::new("/tmp/from_bash.txt");
2854
2855 bash.exec("echo 'Created by bash' > /tmp/from_bash.txt")
2857 .await
2858 .unwrap();
2859
2860 let content = bash.fs().read_file(path).await.unwrap();
2862 assert_eq!(content, b"Created by bash\n");
2863 }
2864
2865 #[tokio::test]
2866 async fn test_fs_exists_and_stat() {
2867 let bash = Bash::new();
2868 let fs = bash.fs();
2869 let path = std::path::Path::new("/tmp/testfile.txt");
2870
2871 assert!(!fs.exists(path).await.unwrap());
2873
2874 fs.write_file(path, b"content").await.unwrap();
2876
2877 assert!(fs.exists(path).await.unwrap());
2879
2880 let stat = fs.stat(path).await.unwrap();
2882 assert!(stat.file_type.is_file());
2883 assert_eq!(stat.size, 7); }
2885
2886 #[tokio::test]
2887 async fn test_fs_mkdir_and_read_dir() {
2888 let bash = Bash::new();
2889 let fs = bash.fs();
2890
2891 fs.mkdir(std::path::Path::new("/data/nested/dir"), true)
2893 .await
2894 .unwrap();
2895
2896 fs.write_file(std::path::Path::new("/data/file1.txt"), b"1")
2898 .await
2899 .unwrap();
2900 fs.write_file(std::path::Path::new("/data/file2.txt"), b"2")
2901 .await
2902 .unwrap();
2903
2904 let entries = fs.read_dir(std::path::Path::new("/data")).await.unwrap();
2906 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
2907 assert!(names.contains(&"nested"));
2908 assert!(names.contains(&"file1.txt"));
2909 assert!(names.contains(&"file2.txt"));
2910 }
2911
2912 #[tokio::test]
2913 async fn test_fs_append() {
2914 let bash = Bash::new();
2915 let fs = bash.fs();
2916 let path = std::path::Path::new("/tmp/append.txt");
2917
2918 fs.write_file(path, b"line1\n").await.unwrap();
2919 fs.append_file(path, b"line2\n").await.unwrap();
2920 fs.append_file(path, b"line3\n").await.unwrap();
2921
2922 let content = fs.read_file(path).await.unwrap();
2923 assert_eq!(content, b"line1\nline2\nline3\n");
2924 }
2925
2926 #[tokio::test]
2927 async fn test_fs_copy_and_rename() {
2928 let bash = Bash::new();
2929 let fs = bash.fs();
2930
2931 fs.write_file(std::path::Path::new("/tmp/original.txt"), b"data")
2932 .await
2933 .unwrap();
2934
2935 fs.copy(
2937 std::path::Path::new("/tmp/original.txt"),
2938 std::path::Path::new("/tmp/copied.txt"),
2939 )
2940 .await
2941 .unwrap();
2942
2943 fs.rename(
2945 std::path::Path::new("/tmp/copied.txt"),
2946 std::path::Path::new("/tmp/renamed.txt"),
2947 )
2948 .await
2949 .unwrap();
2950
2951 let content = fs
2953 .read_file(std::path::Path::new("/tmp/renamed.txt"))
2954 .await
2955 .unwrap();
2956 assert_eq!(content, b"data");
2957 assert!(
2958 !fs.exists(std::path::Path::new("/tmp/copied.txt"))
2959 .await
2960 .unwrap()
2961 );
2962 }
2963
2964 #[tokio::test]
2967 async fn test_echo_done_as_argument() {
2968 let mut bash = Bash::new();
2970 let result = bash
2971 .exec("for i in 1; do echo $i; done; echo done")
2972 .await
2973 .unwrap();
2974 assert_eq!(result.stdout, "1\ndone\n");
2975 }
2976
2977 #[tokio::test]
2978 async fn test_simple_echo_done() {
2979 let mut bash = Bash::new();
2981 let result = bash.exec("echo done").await.unwrap();
2982 assert_eq!(result.stdout, "done\n");
2983 }
2984
2985 #[tokio::test]
2986 async fn test_dev_null_redirect() {
2987 let mut bash = Bash::new();
2989 let result = bash.exec("echo hello > /dev/null; echo ok").await.unwrap();
2990 assert_eq!(result.stdout, "ok\n");
2991 }
2992
2993 #[tokio::test]
2994 async fn test_string_concatenation_in_loop() {
2995 let mut bash = Bash::new();
2997 let result = bash.exec("for i in a b c; do echo $i; done").await.unwrap();
2999 assert_eq!(result.stdout, "a\nb\nc\n");
3000
3001 let mut bash = Bash::new();
3003 let result = bash
3004 .exec("result=x; for i in a b c; do echo $i; done; echo $result")
3005 .await
3006 .unwrap();
3007 assert_eq!(result.stdout, "a\nb\nc\nx\n");
3008
3009 let mut bash = Bash::new();
3011 let result = bash
3012 .exec("result=start; for i in a b c; do result=${result}$i; done; echo $result")
3013 .await
3014 .unwrap();
3015 assert_eq!(result.stdout, "startabc\n");
3016 }
3017
3018 #[tokio::test]
3021 async fn test_done_still_terminates_loop() {
3022 let mut bash = Bash::new();
3024 let result = bash.exec("for i in 1 2; do echo $i; done").await.unwrap();
3025 assert_eq!(result.stdout, "1\n2\n");
3026 }
3027
3028 #[tokio::test]
3029 async fn test_fi_still_terminates_if() {
3030 let mut bash = Bash::new();
3032 let result = bash.exec("if true; then echo yes; fi").await.unwrap();
3033 assert_eq!(result.stdout, "yes\n");
3034 }
3035
3036 #[tokio::test]
3037 async fn test_echo_fi_as_argument() {
3038 let mut bash = Bash::new();
3040 let result = bash.exec("echo fi").await.unwrap();
3041 assert_eq!(result.stdout, "fi\n");
3042 }
3043
3044 #[tokio::test]
3045 async fn test_echo_then_as_argument() {
3046 let mut bash = Bash::new();
3048 let result = bash.exec("echo then").await.unwrap();
3049 assert_eq!(result.stdout, "then\n");
3050 }
3051
3052 #[tokio::test]
3053 async fn test_reserved_words_in_quotes_are_arguments() {
3054 let mut bash = Bash::new();
3056 let result = bash.exec("echo 'done' 'fi' 'then'").await.unwrap();
3057 assert_eq!(result.stdout, "done fi then\n");
3058 }
3059
3060 #[tokio::test]
3061 async fn test_nested_loops_done_keyword() {
3062 let mut bash = Bash::new();
3064 let result = bash
3065 .exec("for i in 1; do for j in a; do echo $i$j; done; done")
3066 .await
3067 .unwrap();
3068 assert_eq!(result.stdout, "1a\n");
3069 }
3070
3071 #[tokio::test]
3074 async fn test_dev_null_read_returns_empty() {
3075 let mut bash = Bash::new();
3077 let result = bash.exec("cat /dev/null").await.unwrap();
3078 assert_eq!(result.stdout, "");
3079 }
3080
3081 #[tokio::test]
3082 async fn test_dev_null_append() {
3083 let mut bash = Bash::new();
3085 let result = bash.exec("echo hello >> /dev/null; echo ok").await.unwrap();
3086 assert_eq!(result.stdout, "ok\n");
3087 }
3088
3089 #[tokio::test]
3090 async fn test_dev_null_in_pipeline() {
3091 let mut bash = Bash::new();
3093 let result = bash
3094 .exec("echo hello | cat > /dev/null; echo ok")
3095 .await
3096 .unwrap();
3097 assert_eq!(result.stdout, "ok\n");
3098 }
3099
3100 #[tokio::test]
3101 async fn test_dev_null_exists() {
3102 let mut bash = Bash::new();
3104 let result = bash.exec("cat /dev/null; echo exit_$?").await.unwrap();
3105 assert_eq!(result.stdout, "exit_0\n");
3106 }
3107
3108 #[tokio::test]
3111 async fn test_custom_username_whoami() {
3112 let mut bash = Bash::builder().username("alice").build();
3113 let result = bash.exec("whoami").await.unwrap();
3114 assert_eq!(result.stdout, "alice\n");
3115 }
3116
3117 #[tokio::test]
3118 async fn test_custom_username_id() {
3119 let mut bash = Bash::builder().username("bob").build();
3120 let result = bash.exec("id").await.unwrap();
3121 assert!(result.stdout.contains("uid=1000(bob)"));
3122 assert!(result.stdout.contains("gid=1000(bob)"));
3123 }
3124
3125 #[tokio::test]
3126 async fn test_custom_username_sets_user_env() {
3127 let mut bash = Bash::builder().username("charlie").build();
3128 let result = bash.exec("echo $USER").await.unwrap();
3129 assert_eq!(result.stdout, "charlie\n");
3130 }
3131
3132 #[tokio::test]
3133 async fn test_custom_hostname() {
3134 let mut bash = Bash::builder().hostname("my-server").build();
3135 let result = bash.exec("hostname").await.unwrap();
3136 assert_eq!(result.stdout, "my-server\n");
3137 }
3138
3139 #[tokio::test]
3140 async fn test_custom_hostname_uname() {
3141 let mut bash = Bash::builder().hostname("custom-host").build();
3142 let result = bash.exec("uname -n").await.unwrap();
3143 assert_eq!(result.stdout, "custom-host\n");
3144 }
3145
3146 #[tokio::test]
3147 async fn test_default_username_and_hostname() {
3148 let mut bash = Bash::new();
3150 let result = bash.exec("whoami").await.unwrap();
3151 assert_eq!(result.stdout, "sandbox\n");
3152
3153 let result = bash.exec("hostname").await.unwrap();
3154 assert_eq!(result.stdout, "bashkit-sandbox\n");
3155 }
3156
3157 #[tokio::test]
3158 async fn test_custom_username_and_hostname_combined() {
3159 let mut bash = Bash::builder()
3160 .username("deploy")
3161 .hostname("prod-server-01")
3162 .build();
3163
3164 let result = bash.exec("whoami && hostname").await.unwrap();
3165 assert_eq!(result.stdout, "deploy\nprod-server-01\n");
3166
3167 let result = bash.exec("echo $USER").await.unwrap();
3168 assert_eq!(result.stdout, "deploy\n");
3169 }
3170
3171 mod custom_builtins {
3174 use super::*;
3175 use crate::ExecResult;
3176 use crate::builtins::{Builtin, Context};
3177 use async_trait::async_trait;
3178
3179 struct Hello;
3181
3182 #[async_trait]
3183 impl Builtin for Hello {
3184 async fn execute(&self, _ctx: Context<'_>) -> crate::Result<ExecResult> {
3185 Ok(ExecResult::ok("Hello from custom builtin!\n".to_string()))
3186 }
3187 }
3188
3189 #[tokio::test]
3190 async fn test_custom_builtin_basic() {
3191 let mut bash = Bash::builder().builtin("hello", Box::new(Hello)).build();
3192
3193 let result = bash.exec("hello").await.unwrap();
3194 assert_eq!(result.stdout, "Hello from custom builtin!\n");
3195 assert_eq!(result.exit_code, 0);
3196 }
3197
3198 struct Greet;
3200
3201 #[async_trait]
3202 impl Builtin for Greet {
3203 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
3204 let name = ctx.args.first().map(|s| s.as_str()).unwrap_or("World");
3205 Ok(ExecResult::ok(format!("Hello, {}!\n", name)))
3206 }
3207 }
3208
3209 #[tokio::test]
3210 async fn test_custom_builtin_with_args() {
3211 let mut bash = Bash::builder().builtin("greet", Box::new(Greet)).build();
3212
3213 let result = bash.exec("greet").await.unwrap();
3214 assert_eq!(result.stdout, "Hello, World!\n");
3215
3216 let result = bash.exec("greet Alice").await.unwrap();
3217 assert_eq!(result.stdout, "Hello, Alice!\n");
3218
3219 let result = bash.exec("greet Bob Charlie").await.unwrap();
3220 assert_eq!(result.stdout, "Hello, Bob!\n");
3221 }
3222
3223 struct Upper;
3225
3226 #[async_trait]
3227 impl Builtin for Upper {
3228 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
3229 let input = ctx.stdin.unwrap_or("");
3230 Ok(ExecResult::ok(input.to_uppercase()))
3231 }
3232 }
3233
3234 #[tokio::test]
3235 async fn test_custom_builtin_with_stdin() {
3236 let mut bash = Bash::builder().builtin("upper", Box::new(Upper)).build();
3237
3238 let result = bash.exec("echo hello | upper").await.unwrap();
3239 assert_eq!(result.stdout, "HELLO\n");
3240 }
3241
3242 struct WriteFile;
3244
3245 #[async_trait]
3246 impl Builtin for WriteFile {
3247 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
3248 if ctx.args.len() < 2 {
3249 return Ok(ExecResult::err(
3250 "Usage: writefile <path> <content>\n".to_string(),
3251 1,
3252 ));
3253 }
3254 let path = std::path::Path::new(&ctx.args[0]);
3255 let content = ctx.args[1..].join(" ");
3256 ctx.fs.write_file(path, content.as_bytes()).await?;
3257 Ok(ExecResult::ok(String::new()))
3258 }
3259 }
3260
3261 #[tokio::test]
3262 async fn test_custom_builtin_with_filesystem() {
3263 let mut bash = Bash::builder()
3264 .builtin("writefile", Box::new(WriteFile))
3265 .build();
3266
3267 bash.exec("writefile /tmp/test.txt custom content here")
3268 .await
3269 .unwrap();
3270
3271 let result = bash.exec("cat /tmp/test.txt").await.unwrap();
3272 assert_eq!(result.stdout, "custom content here");
3273 }
3274
3275 struct CustomEcho;
3277
3278 #[async_trait]
3279 impl Builtin for CustomEcho {
3280 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
3281 let msg = ctx.args.join(" ");
3282 Ok(ExecResult::ok(format!("[CUSTOM] {}\n", msg)))
3283 }
3284 }
3285
3286 #[tokio::test]
3287 async fn test_custom_builtin_override_default() {
3288 let mut bash = Bash::builder()
3289 .builtin("echo", Box::new(CustomEcho))
3290 .build();
3291
3292 let result = bash.exec("echo hello world").await.unwrap();
3293 assert_eq!(result.stdout, "[CUSTOM] hello world\n");
3294 }
3295
3296 #[tokio::test]
3298 async fn test_multiple_custom_builtins() {
3299 let mut bash = Bash::builder()
3300 .builtin("hello", Box::new(Hello))
3301 .builtin("greet", Box::new(Greet))
3302 .builtin("upper", Box::new(Upper))
3303 .build();
3304
3305 let result = bash.exec("hello").await.unwrap();
3306 assert_eq!(result.stdout, "Hello from custom builtin!\n");
3307
3308 let result = bash.exec("greet Test").await.unwrap();
3309 assert_eq!(result.stdout, "Hello, Test!\n");
3310
3311 let result = bash.exec("echo foo | upper").await.unwrap();
3312 assert_eq!(result.stdout, "FOO\n");
3313 }
3314
3315 struct Counter {
3317 prefix: String,
3318 }
3319
3320 #[async_trait]
3321 impl Builtin for Counter {
3322 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
3323 let count = ctx
3324 .args
3325 .first()
3326 .and_then(|s| s.parse::<i32>().ok())
3327 .unwrap_or(1);
3328 let mut output = String::new();
3329 for i in 1..=count {
3330 output.push_str(&format!("{}{}\n", self.prefix, i));
3331 }
3332 Ok(ExecResult::ok(output))
3333 }
3334 }
3335
3336 #[tokio::test]
3337 async fn test_custom_builtin_with_state() {
3338 let mut bash = Bash::builder()
3339 .builtin(
3340 "count",
3341 Box::new(Counter {
3342 prefix: "Item ".to_string(),
3343 }),
3344 )
3345 .build();
3346
3347 let result = bash.exec("count 3").await.unwrap();
3348 assert_eq!(result.stdout, "Item 1\nItem 2\nItem 3\n");
3349 }
3350
3351 struct Fail;
3353
3354 #[async_trait]
3355 impl Builtin for Fail {
3356 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
3357 let code = ctx
3358 .args
3359 .first()
3360 .and_then(|s| s.parse::<i32>().ok())
3361 .unwrap_or(1);
3362 Ok(ExecResult::err(
3363 format!("Failed with code {}\n", code),
3364 code,
3365 ))
3366 }
3367 }
3368
3369 #[tokio::test]
3370 async fn test_custom_builtin_error() {
3371 let mut bash = Bash::builder().builtin("fail", Box::new(Fail)).build();
3372
3373 let result = bash.exec("fail 42").await.unwrap();
3374 assert_eq!(result.exit_code, 42);
3375 assert_eq!(result.stderr, "Failed with code 42\n");
3376 }
3377
3378 #[tokio::test]
3379 async fn test_custom_builtin_in_script() {
3380 let mut bash = Bash::builder().builtin("greet", Box::new(Greet)).build();
3381
3382 let script = r#"
3383 for name in Alice Bob Charlie; do
3384 greet $name
3385 done
3386 "#;
3387
3388 let result = bash.exec(script).await.unwrap();
3389 assert_eq!(
3390 result.stdout,
3391 "Hello, Alice!\nHello, Bob!\nHello, Charlie!\n"
3392 );
3393 }
3394
3395 #[tokio::test]
3396 async fn test_custom_builtin_with_conditionals() {
3397 let mut bash = Bash::builder()
3398 .builtin("fail", Box::new(Fail))
3399 .builtin("hello", Box::new(Hello))
3400 .build();
3401
3402 let result = bash.exec("fail 1 || hello").await.unwrap();
3403 assert_eq!(result.stdout, "Hello from custom builtin!\n");
3404 assert_eq!(result.exit_code, 0);
3405
3406 let result = bash.exec("hello && fail 5").await.unwrap();
3407 assert_eq!(result.exit_code, 5);
3408 }
3409
3410 struct EnvReader;
3412
3413 #[async_trait]
3414 impl Builtin for EnvReader {
3415 async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
3416 let var_name = ctx.args.first().map(|s| s.as_str()).unwrap_or("HOME");
3417 let value = ctx
3418 .env
3419 .get(var_name)
3420 .map(|s| s.as_str())
3421 .unwrap_or("(not set)");
3422 Ok(ExecResult::ok(format!("{}={}\n", var_name, value)))
3423 }
3424 }
3425
3426 #[tokio::test]
3427 async fn test_custom_builtin_reads_env() {
3428 let mut bash = Bash::builder()
3429 .env("MY_VAR", "my_value")
3430 .builtin("readenv", Box::new(EnvReader))
3431 .build();
3432
3433 let result = bash.exec("readenv MY_VAR").await.unwrap();
3434 assert_eq!(result.stdout, "MY_VAR=my_value\n");
3435
3436 let result = bash.exec("readenv UNKNOWN").await.unwrap();
3437 assert_eq!(result.stdout, "UNKNOWN=(not set)\n");
3438 }
3439 }
3440
3441 #[tokio::test]
3444 async fn test_parser_timeout_default() {
3445 let limits = ExecutionLimits::default();
3447 assert_eq!(limits.parser_timeout, std::time::Duration::from_secs(5));
3448 }
3449
3450 #[tokio::test]
3451 async fn test_parser_timeout_custom() {
3452 let limits = ExecutionLimits::new().parser_timeout(std::time::Duration::from_millis(100));
3454 assert_eq!(limits.parser_timeout, std::time::Duration::from_millis(100));
3455 }
3456
3457 #[tokio::test]
3458 async fn test_parser_timeout_normal_script() {
3459 let limits = ExecutionLimits::new().parser_timeout(std::time::Duration::from_secs(1));
3461 let mut bash = Bash::builder().limits(limits).build();
3462 let result = bash.exec("echo hello").await.unwrap();
3463 assert_eq!(result.stdout, "hello\n");
3464 }
3465
3466 #[tokio::test]
3469 async fn test_parser_fuel_default() {
3470 let limits = ExecutionLimits::default();
3472 assert_eq!(limits.max_parser_operations, 100_000);
3473 }
3474
3475 #[tokio::test]
3476 async fn test_parser_fuel_custom() {
3477 let limits = ExecutionLimits::new().max_parser_operations(1000);
3479 assert_eq!(limits.max_parser_operations, 1000);
3480 }
3481
3482 #[tokio::test]
3483 async fn test_parser_fuel_normal_script() {
3484 let limits = ExecutionLimits::new().max_parser_operations(1000);
3486 let mut bash = Bash::builder().limits(limits).build();
3487 let result = bash.exec("echo hello").await.unwrap();
3488 assert_eq!(result.stdout, "hello\n");
3489 }
3490
3491 #[tokio::test]
3494 async fn test_input_size_limit_default() {
3495 let limits = ExecutionLimits::default();
3497 assert_eq!(limits.max_input_bytes, 10_000_000);
3498 }
3499
3500 #[tokio::test]
3501 async fn test_input_size_limit_custom() {
3502 let limits = ExecutionLimits::new().max_input_bytes(1000);
3504 assert_eq!(limits.max_input_bytes, 1000);
3505 }
3506
3507 #[tokio::test]
3508 async fn test_input_size_limit_enforced() {
3509 let limits = ExecutionLimits::new().max_input_bytes(10);
3511 let mut bash = Bash::builder().limits(limits).build();
3512
3513 let result = bash.exec("echo hello world").await;
3515 assert!(result.is_err());
3516 let err = result.unwrap_err();
3517 assert!(
3518 err.to_string().contains("input too large"),
3519 "Expected input size error, got: {}",
3520 err
3521 );
3522 }
3523
3524 #[tokio::test]
3525 async fn test_input_size_limit_normal_script() {
3526 let limits = ExecutionLimits::new().max_input_bytes(1000);
3528 let mut bash = Bash::builder().limits(limits).build();
3529 let result = bash.exec("echo hello").await.unwrap();
3530 assert_eq!(result.stdout, "hello\n");
3531 }
3532
3533 #[tokio::test]
3536 async fn test_ast_depth_limit_default() {
3537 let limits = ExecutionLimits::default();
3539 assert_eq!(limits.max_ast_depth, 100);
3540 }
3541
3542 #[tokio::test]
3543 async fn test_ast_depth_limit_custom() {
3544 let limits = ExecutionLimits::new().max_ast_depth(10);
3546 assert_eq!(limits.max_ast_depth, 10);
3547 }
3548
3549 #[tokio::test]
3550 async fn test_ast_depth_limit_normal_script() {
3551 let limits = ExecutionLimits::new().max_ast_depth(10);
3553 let mut bash = Bash::builder().limits(limits).build();
3554 let result = bash.exec("if true; then echo ok; fi").await.unwrap();
3555 assert_eq!(result.stdout, "ok\n");
3556 }
3557
3558 #[tokio::test]
3559 async fn test_ast_depth_limit_enforced() {
3560 let limits = ExecutionLimits::new().max_ast_depth(2);
3562 let mut bash = Bash::builder().limits(limits).build();
3563
3564 let result = bash
3566 .exec("if true; then if true; then if true; then echo nested; fi; fi; fi")
3567 .await;
3568 assert!(result.is_err());
3569 let err = result.unwrap_err();
3570 assert!(
3571 err.to_string().contains("AST nesting too deep"),
3572 "Expected AST depth error, got: {}",
3573 err
3574 );
3575 }
3576
3577 #[tokio::test]
3578 async fn test_parser_fuel_enforced() {
3579 let limits = ExecutionLimits::new().max_parser_operations(3);
3582 let mut bash = Bash::builder().limits(limits).build();
3583
3584 let result = bash.exec("echo a; echo b; echo c").await;
3586 assert!(result.is_err());
3587 let err = result.unwrap_err();
3588 assert!(
3589 err.to_string().contains("parser fuel exhausted"),
3590 "Expected parser fuel error, got: {}",
3591 err
3592 );
3593 }
3594
3595 #[tokio::test]
3598 async fn test_set_e_basic() {
3599 let mut bash = Bash::new();
3601 let result = bash
3602 .exec("set -e; true; false; echo should_not_reach")
3603 .await
3604 .unwrap();
3605 assert_eq!(result.stdout, "");
3606 assert_eq!(result.exit_code, 1);
3607 }
3608
3609 #[tokio::test]
3610 async fn test_set_e_after_failing_cmd() {
3611 let mut bash = Bash::new();
3613 let result = bash
3614 .exec("set -e; echo before; false; echo after")
3615 .await
3616 .unwrap();
3617 assert_eq!(result.stdout, "before\n");
3618 assert_eq!(result.exit_code, 1);
3619 }
3620
3621 #[tokio::test]
3622 async fn test_set_e_disabled() {
3623 let mut bash = Bash::new();
3625 let result = bash
3626 .exec("set -e; set +e; false; echo still_running")
3627 .await
3628 .unwrap();
3629 assert_eq!(result.stdout, "still_running\n");
3630 }
3631
3632 #[tokio::test]
3633 async fn test_set_e_in_pipeline_last() {
3634 let mut bash = Bash::new();
3636 let result = bash
3637 .exec("set -e; false | true; echo reached")
3638 .await
3639 .unwrap();
3640 assert_eq!(result.stdout, "reached\n");
3641 }
3642
3643 #[tokio::test]
3644 async fn test_set_e_in_if_condition() {
3645 let mut bash = Bash::new();
3647 let result = bash
3648 .exec("set -e; if false; then echo yes; else echo no; fi; echo done")
3649 .await
3650 .unwrap();
3651 assert_eq!(result.stdout, "no\ndone\n");
3652 }
3653
3654 #[tokio::test]
3655 async fn test_set_e_in_while_condition() {
3656 let mut bash = Bash::new();
3658 let result = bash
3659 .exec("set -e; x=0; while [ \"$x\" -lt 2 ]; do echo \"x=$x\"; x=$((x + 1)); done; echo done")
3660 .await
3661 .unwrap();
3662 assert_eq!(result.stdout, "x=0\nx=1\ndone\n");
3663 }
3664
3665 #[tokio::test]
3666 async fn test_set_e_in_brace_group() {
3667 let mut bash = Bash::new();
3669 let result = bash
3670 .exec("set -e; { echo start; false; echo unreached; }; echo after")
3671 .await
3672 .unwrap();
3673 assert_eq!(result.stdout, "start\n");
3674 assert_eq!(result.exit_code, 1);
3675 }
3676
3677 #[tokio::test]
3678 async fn test_set_e_and_chain() {
3679 let mut bash = Bash::new();
3681 let result = bash
3682 .exec("set -e; false && echo one; echo reached")
3683 .await
3684 .unwrap();
3685 assert_eq!(result.stdout, "reached\n");
3686 }
3687
3688 #[tokio::test]
3689 async fn test_set_e_or_chain() {
3690 let mut bash = Bash::new();
3692 let result = bash
3693 .exec("set -e; true || false; echo reached")
3694 .await
3695 .unwrap();
3696 assert_eq!(result.stdout, "reached\n");
3697 }
3698
3699 #[tokio::test]
3702 async fn test_tilde_expansion_basic() {
3703 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3705 let result = bash.exec("echo ~").await.unwrap();
3706 assert_eq!(result.stdout, "/home/testuser\n");
3707 }
3708
3709 #[tokio::test]
3710 async fn test_tilde_expansion_with_path() {
3711 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3713 let result = bash.exec("echo ~/documents/file.txt").await.unwrap();
3714 assert_eq!(result.stdout, "/home/testuser/documents/file.txt\n");
3715 }
3716
3717 #[tokio::test]
3718 async fn test_tilde_expansion_in_assignment() {
3719 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3721 let result = bash.exec("DIR=~/data; echo $DIR").await.unwrap();
3722 assert_eq!(result.stdout, "/home/testuser/data\n");
3723 }
3724
3725 #[tokio::test]
3726 async fn test_tilde_expansion_default_home() {
3727 let mut bash = Bash::new();
3729 let result = bash.exec("echo ~").await.unwrap();
3730 assert_eq!(result.stdout, "/home/sandbox\n");
3731 }
3732
3733 #[tokio::test]
3734 async fn test_tilde_not_at_start() {
3735 let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3737 let result = bash.exec("echo foo~bar").await.unwrap();
3738 assert_eq!(result.stdout, "foo~bar\n");
3739 }
3740
3741 #[tokio::test]
3744 async fn test_special_var_dollar_dollar() {
3745 let mut bash = Bash::new();
3747 let result = bash.exec("echo $$").await.unwrap();
3748 let pid: u32 = result.stdout.trim().parse().expect("$$ should be a number");
3750 assert!(pid > 0, "$$ should be a positive number");
3751 }
3752
3753 #[tokio::test]
3754 async fn test_special_var_random() {
3755 let mut bash = Bash::new();
3757 let result = bash.exec("echo $RANDOM").await.unwrap();
3758 let random: u32 = result
3759 .stdout
3760 .trim()
3761 .parse()
3762 .expect("$RANDOM should be a number");
3763 assert!(random < 32768, "$RANDOM should be < 32768");
3764 }
3765
3766 #[tokio::test]
3767 async fn test_special_var_random_varies() {
3768 let mut bash = Bash::new();
3770 let result1 = bash.exec("echo $RANDOM").await.unwrap();
3771 let result2 = bash.exec("echo $RANDOM").await.unwrap();
3772 let _: u32 = result1
3776 .stdout
3777 .trim()
3778 .parse()
3779 .expect("$RANDOM should be a number");
3780 let _: u32 = result2
3781 .stdout
3782 .trim()
3783 .parse()
3784 .expect("$RANDOM should be a number");
3785 }
3786
3787 #[tokio::test]
3788 async fn test_special_var_lineno() {
3789 let mut bash = Bash::new();
3791 let result = bash.exec("echo $LINENO").await.unwrap();
3792 assert_eq!(result.stdout, "1\n");
3793 }
3794
3795 #[tokio::test]
3796 async fn test_lineno_multiline() {
3797 let mut bash = Bash::new();
3799 let result = bash
3800 .exec(
3801 r#"echo "line $LINENO"
3802echo "line $LINENO"
3803echo "line $LINENO""#,
3804 )
3805 .await
3806 .unwrap();
3807 assert_eq!(result.stdout, "line 1\nline 2\nline 3\n");
3808 }
3809
3810 #[tokio::test]
3811 async fn test_lineno_in_loop() {
3812 let mut bash = Bash::new();
3814 let result = bash
3815 .exec(
3816 r#"for i in 1 2; do
3817 echo "loop $LINENO"
3818done"#,
3819 )
3820 .await
3821 .unwrap();
3822 assert_eq!(result.stdout, "loop 2\nloop 2\n");
3824 }
3825
3826 #[tokio::test]
3829 async fn test_file_test_r_readable() {
3830 let mut bash = Bash::new();
3832 bash.exec("echo hello > /tmp/readable.txt").await.unwrap();
3833 let result = bash
3834 .exec("test -r /tmp/readable.txt && echo yes")
3835 .await
3836 .unwrap();
3837 assert_eq!(result.stdout, "yes\n");
3838 }
3839
3840 #[tokio::test]
3841 async fn test_file_test_r_not_exists() {
3842 let mut bash = Bash::new();
3844 let result = bash
3845 .exec("test -r /tmp/nonexistent.txt && echo yes || echo no")
3846 .await
3847 .unwrap();
3848 assert_eq!(result.stdout, "no\n");
3849 }
3850
3851 #[tokio::test]
3852 async fn test_file_test_w_writable() {
3853 let mut bash = Bash::new();
3855 bash.exec("echo hello > /tmp/writable.txt").await.unwrap();
3856 let result = bash
3857 .exec("test -w /tmp/writable.txt && echo yes")
3858 .await
3859 .unwrap();
3860 assert_eq!(result.stdout, "yes\n");
3861 }
3862
3863 #[tokio::test]
3864 async fn test_file_test_x_executable() {
3865 let mut bash = Bash::new();
3867 bash.exec("echo '#!/bin/bash' > /tmp/script.sh")
3868 .await
3869 .unwrap();
3870 bash.exec("chmod 755 /tmp/script.sh").await.unwrap();
3871 let result = bash
3872 .exec("test -x /tmp/script.sh && echo yes")
3873 .await
3874 .unwrap();
3875 assert_eq!(result.stdout, "yes\n");
3876 }
3877
3878 #[tokio::test]
3879 async fn test_file_test_x_not_executable() {
3880 let mut bash = Bash::new();
3882 bash.exec("echo 'data' > /tmp/noexec.txt").await.unwrap();
3883 bash.exec("chmod 644 /tmp/noexec.txt").await.unwrap();
3884 let result = bash
3885 .exec("test -x /tmp/noexec.txt && echo yes || echo no")
3886 .await
3887 .unwrap();
3888 assert_eq!(result.stdout, "no\n");
3889 }
3890
3891 #[tokio::test]
3892 async fn test_file_test_e_exists() {
3893 let mut bash = Bash::new();
3895 bash.exec("echo hello > /tmp/exists.txt").await.unwrap();
3896 let result = bash
3897 .exec("test -e /tmp/exists.txt && echo yes")
3898 .await
3899 .unwrap();
3900 assert_eq!(result.stdout, "yes\n");
3901 }
3902
3903 #[tokio::test]
3904 async fn test_file_test_f_regular() {
3905 let mut bash = Bash::new();
3907 bash.exec("echo hello > /tmp/regular.txt").await.unwrap();
3908 let result = bash
3909 .exec("test -f /tmp/regular.txt && echo yes")
3910 .await
3911 .unwrap();
3912 assert_eq!(result.stdout, "yes\n");
3913 }
3914
3915 #[tokio::test]
3916 async fn test_file_test_d_directory() {
3917 let mut bash = Bash::new();
3919 bash.exec("mkdir -p /tmp/mydir").await.unwrap();
3920 let result = bash.exec("test -d /tmp/mydir && echo yes").await.unwrap();
3921 assert_eq!(result.stdout, "yes\n");
3922 }
3923
3924 #[tokio::test]
3925 async fn test_file_test_s_size() {
3926 let mut bash = Bash::new();
3928 bash.exec("echo hello > /tmp/nonempty.txt").await.unwrap();
3929 let result = bash
3930 .exec("test -s /tmp/nonempty.txt && echo yes")
3931 .await
3932 .unwrap();
3933 assert_eq!(result.stdout, "yes\n");
3934 }
3935
3936 #[tokio::test]
3941 async fn test_redirect_both_stdout_stderr() {
3942 let mut bash = Bash::new();
3944 let result = bash.exec("echo hello &> /tmp/out.txt").await.unwrap();
3946 assert_eq!(result.stdout, "");
3948 let check = bash.exec("cat /tmp/out.txt").await.unwrap();
3950 assert_eq!(check.stdout, "hello\n");
3951 }
3952
3953 #[tokio::test]
3954 async fn test_stderr_redirect_to_file() {
3955 let mut bash = Bash::new();
3959 bash.exec("echo stdout; echo stderr 2> /tmp/err.txt")
3961 .await
3962 .unwrap();
3963 }
3966
3967 #[tokio::test]
3968 async fn test_fd_redirect_parsing() {
3969 let mut bash = Bash::new();
3971 let result = bash.exec("true 2> /tmp/err.txt").await.unwrap();
3973 assert_eq!(result.exit_code, 0);
3974 }
3975
3976 #[tokio::test]
3977 async fn test_fd_redirect_append_parsing() {
3978 let mut bash = Bash::new();
3980 let result = bash.exec("true 2>> /tmp/err.txt").await.unwrap();
3981 assert_eq!(result.exit_code, 0);
3982 }
3983
3984 #[tokio::test]
3985 async fn test_fd_dup_parsing() {
3986 let mut bash = Bash::new();
3988 let result = bash.exec("echo hello 2>&1").await.unwrap();
3989 assert_eq!(result.stdout, "hello\n");
3990 assert_eq!(result.exit_code, 0);
3991 }
3992
3993 #[tokio::test]
3994 async fn test_dup_output_redirect_stdout_to_stderr() {
3995 let mut bash = Bash::new();
3997 let result = bash.exec("echo hello >&2").await.unwrap();
3998 assert_eq!(result.stdout, "");
4000 assert_eq!(result.stderr, "hello\n");
4001 }
4002
4003 #[tokio::test]
4004 async fn test_lexer_redirect_both() {
4005 let mut bash = Bash::new();
4007 let result = bash.exec("echo test &> /tmp/both.txt").await.unwrap();
4009 assert_eq!(result.stdout, "");
4010 let check = bash.exec("cat /tmp/both.txt").await.unwrap();
4011 assert_eq!(check.stdout, "test\n");
4012 }
4013
4014 #[tokio::test]
4015 async fn test_lexer_dup_output() {
4016 let mut bash = Bash::new();
4018 let result = bash.exec("echo test >&2").await.unwrap();
4019 assert_eq!(result.stdout, "");
4020 assert_eq!(result.stderr, "test\n");
4021 }
4022
4023 #[tokio::test]
4024 async fn test_digit_before_redirect() {
4025 let mut bash = Bash::new();
4027 let result = bash.exec("echo hello 2> /tmp/err.txt").await.unwrap();
4029 assert_eq!(result.exit_code, 0);
4030 assert_eq!(result.stdout, "hello\n");
4032 }
4033
4034 #[tokio::test]
4039 async fn test_arithmetic_logical_and_true() {
4040 let mut bash = Bash::new();
4042 let result = bash.exec("echo $((1 && 1))").await.unwrap();
4043 assert_eq!(result.stdout, "1\n");
4044 }
4045
4046 #[tokio::test]
4047 async fn test_arithmetic_logical_and_false_left() {
4048 let mut bash = Bash::new();
4050 let result = bash.exec("echo $((0 && 1))").await.unwrap();
4051 assert_eq!(result.stdout, "0\n");
4052 }
4053
4054 #[tokio::test]
4055 async fn test_arithmetic_logical_and_false_right() {
4056 let mut bash = Bash::new();
4058 let result = bash.exec("echo $((1 && 0))").await.unwrap();
4059 assert_eq!(result.stdout, "0\n");
4060 }
4061
4062 #[tokio::test]
4063 async fn test_arithmetic_logical_or_false() {
4064 let mut bash = Bash::new();
4066 let result = bash.exec("echo $((0 || 0))").await.unwrap();
4067 assert_eq!(result.stdout, "0\n");
4068 }
4069
4070 #[tokio::test]
4071 async fn test_arithmetic_logical_or_true_left() {
4072 let mut bash = Bash::new();
4074 let result = bash.exec("echo $((1 || 0))").await.unwrap();
4075 assert_eq!(result.stdout, "1\n");
4076 }
4077
4078 #[tokio::test]
4079 async fn test_arithmetic_logical_or_true_right() {
4080 let mut bash = Bash::new();
4082 let result = bash.exec("echo $((0 || 1))").await.unwrap();
4083 assert_eq!(result.stdout, "1\n");
4084 }
4085
4086 #[tokio::test]
4087 async fn test_arithmetic_logical_combined() {
4088 let mut bash = Bash::new();
4090 let result = bash.exec("echo $((5 > 3 && 2 < 4))").await.unwrap();
4092 assert_eq!(result.stdout, "1\n");
4093 }
4094
4095 #[tokio::test]
4096 async fn test_arithmetic_logical_with_comparison() {
4097 let mut bash = Bash::new();
4099 let result = bash.exec("echo $((5 < 3 || 2 < 4))").await.unwrap();
4101 assert_eq!(result.stdout, "1\n");
4102 }
4103
4104 #[tokio::test]
4105 async fn test_arithmetic_multibyte_no_panic() {
4106 let mut bash = Bash::new();
4108 let result = bash.exec("echo $((0,1))").await.unwrap();
4110 assert_eq!(result.stdout, "1\n");
4111 let _ = bash.exec("echo $((\u{00e9}+1))").await;
4113 }
4114
4115 #[tokio::test]
4120 async fn test_brace_expansion_list() {
4121 let mut bash = Bash::new();
4123 let result = bash.exec("echo {a,b,c}").await.unwrap();
4124 assert_eq!(result.stdout, "a b c\n");
4125 }
4126
4127 #[tokio::test]
4128 async fn test_brace_expansion_with_prefix() {
4129 let mut bash = Bash::new();
4131 let result = bash.exec("echo file{1,2,3}.txt").await.unwrap();
4132 assert_eq!(result.stdout, "file1.txt file2.txt file3.txt\n");
4133 }
4134
4135 #[tokio::test]
4136 async fn test_brace_expansion_numeric_range() {
4137 let mut bash = Bash::new();
4139 let result = bash.exec("echo {1..5}").await.unwrap();
4140 assert_eq!(result.stdout, "1 2 3 4 5\n");
4141 }
4142
4143 #[tokio::test]
4144 async fn test_brace_expansion_char_range() {
4145 let mut bash = Bash::new();
4147 let result = bash.exec("echo {a..e}").await.unwrap();
4148 assert_eq!(result.stdout, "a b c d e\n");
4149 }
4150
4151 #[tokio::test]
4152 async fn test_brace_expansion_reverse_range() {
4153 let mut bash = Bash::new();
4155 let result = bash.exec("echo {5..1}").await.unwrap();
4156 assert_eq!(result.stdout, "5 4 3 2 1\n");
4157 }
4158
4159 #[tokio::test]
4160 async fn test_brace_expansion_nested() {
4161 let mut bash = Bash::new();
4163 let result = bash.exec("echo {a,b}{1,2}").await.unwrap();
4164 assert_eq!(result.stdout, "a1 a2 b1 b2\n");
4165 }
4166
4167 #[tokio::test]
4168 async fn test_brace_expansion_with_suffix() {
4169 let mut bash = Bash::new();
4171 let result = bash.exec("echo pre{x,y}suf").await.unwrap();
4172 assert_eq!(result.stdout, "prexsuf preysuf\n");
4173 }
4174
4175 #[tokio::test]
4176 async fn test_brace_expansion_empty_item() {
4177 let mut bash = Bash::new();
4179 let result = bash.exec("echo x{,y}z").await.unwrap();
4180 assert_eq!(result.stdout, "xz xyz\n");
4181 }
4182
4183 #[tokio::test]
4188 async fn test_string_less_than() {
4189 let mut bash = Bash::new();
4190 let result = bash
4191 .exec("test apple '<' banana && echo yes")
4192 .await
4193 .unwrap();
4194 assert_eq!(result.stdout, "yes\n");
4195 }
4196
4197 #[tokio::test]
4198 async fn test_string_greater_than() {
4199 let mut bash = Bash::new();
4200 let result = bash
4201 .exec("test banana '>' apple && echo yes")
4202 .await
4203 .unwrap();
4204 assert_eq!(result.stdout, "yes\n");
4205 }
4206
4207 #[tokio::test]
4208 async fn test_string_less_than_false() {
4209 let mut bash = Bash::new();
4210 let result = bash
4211 .exec("test banana '<' apple && echo yes || echo no")
4212 .await
4213 .unwrap();
4214 assert_eq!(result.stdout, "no\n");
4215 }
4216
4217 #[tokio::test]
4222 async fn test_array_indices_basic() {
4223 let mut bash = Bash::new();
4225 let result = bash.exec("arr=(a b c); echo ${!arr[@]}").await.unwrap();
4226 assert_eq!(result.stdout, "0 1 2\n");
4227 }
4228
4229 #[tokio::test]
4230 async fn test_array_indices_sparse() {
4231 let mut bash = Bash::new();
4233 let result = bash
4234 .exec("arr[0]=a; arr[5]=b; arr[10]=c; echo ${!arr[@]}")
4235 .await
4236 .unwrap();
4237 assert_eq!(result.stdout, "0 5 10\n");
4238 }
4239
4240 #[tokio::test]
4241 async fn test_array_indices_star() {
4242 let mut bash = Bash::new();
4244 let result = bash.exec("arr=(x y z); echo ${!arr[*]}").await.unwrap();
4245 assert_eq!(result.stdout, "0 1 2\n");
4246 }
4247
4248 #[tokio::test]
4249 async fn test_array_indices_empty() {
4250 let mut bash = Bash::new();
4252 let result = bash.exec("arr=(); echo \"${!arr[@]}\"").await.unwrap();
4253 assert_eq!(result.stdout, "\n");
4254 }
4255
4256 #[tokio::test]
4261 async fn test_text_file_basic() {
4262 let mut bash = Bash::builder()
4263 .mount_text("/config/app.conf", "debug=true\nport=8080\n")
4264 .build();
4265
4266 let result = bash.exec("cat /config/app.conf").await.unwrap();
4267 assert_eq!(result.stdout, "debug=true\nport=8080\n");
4268 }
4269
4270 #[tokio::test]
4271 async fn test_text_file_multiple() {
4272 let mut bash = Bash::builder()
4273 .mount_text("/data/file1.txt", "content one")
4274 .mount_text("/data/file2.txt", "content two")
4275 .mount_text("/other/file3.txt", "content three")
4276 .build();
4277
4278 let result = bash.exec("cat /data/file1.txt").await.unwrap();
4279 assert_eq!(result.stdout, "content one");
4280
4281 let result = bash.exec("cat /data/file2.txt").await.unwrap();
4282 assert_eq!(result.stdout, "content two");
4283
4284 let result = bash.exec("cat /other/file3.txt").await.unwrap();
4285 assert_eq!(result.stdout, "content three");
4286 }
4287
4288 #[tokio::test]
4289 async fn test_text_file_nested_directory() {
4290 let mut bash = Bash::builder()
4292 .mount_text("/a/b/c/d/file.txt", "nested content")
4293 .build();
4294
4295 let result = bash.exec("cat /a/b/c/d/file.txt").await.unwrap();
4296 assert_eq!(result.stdout, "nested content");
4297 }
4298
4299 #[tokio::test]
4300 async fn test_text_file_mode() {
4301 let bash = Bash::builder()
4302 .mount_text("/tmp/writable.txt", "content")
4303 .build();
4304
4305 let stat = bash
4306 .fs()
4307 .stat(std::path::Path::new("/tmp/writable.txt"))
4308 .await
4309 .unwrap();
4310 assert_eq!(stat.mode, 0o644);
4311 }
4312
4313 #[tokio::test]
4314 async fn test_readonly_text_basic() {
4315 let mut bash = Bash::builder()
4316 .mount_readonly_text("/etc/version", "1.2.3")
4317 .build();
4318
4319 let result = bash.exec("cat /etc/version").await.unwrap();
4320 assert_eq!(result.stdout, "1.2.3");
4321 }
4322
4323 #[tokio::test]
4324 async fn test_readonly_text_mode() {
4325 let bash = Bash::builder()
4326 .mount_readonly_text("/etc/readonly.conf", "immutable")
4327 .build();
4328
4329 let stat = bash
4330 .fs()
4331 .stat(std::path::Path::new("/etc/readonly.conf"))
4332 .await
4333 .unwrap();
4334 assert_eq!(stat.mode, 0o444);
4335 }
4336
4337 #[tokio::test]
4338 async fn test_text_file_mixed_readonly_writable() {
4339 let bash = Bash::builder()
4340 .mount_text("/data/writable.txt", "can edit")
4341 .mount_readonly_text("/data/readonly.txt", "cannot edit")
4342 .build();
4343
4344 let writable_stat = bash
4345 .fs()
4346 .stat(std::path::Path::new("/data/writable.txt"))
4347 .await
4348 .unwrap();
4349 let readonly_stat = bash
4350 .fs()
4351 .stat(std::path::Path::new("/data/readonly.txt"))
4352 .await
4353 .unwrap();
4354
4355 assert_eq!(writable_stat.mode, 0o644);
4356 assert_eq!(readonly_stat.mode, 0o444);
4357 }
4358
4359 #[tokio::test]
4360 async fn test_text_file_with_env() {
4361 let mut bash = Bash::builder()
4363 .env("APP_NAME", "testapp")
4364 .mount_text("/config/app.conf", "name=${APP_NAME}")
4365 .build();
4366
4367 let result = bash.exec("echo $APP_NAME").await.unwrap();
4368 assert_eq!(result.stdout, "testapp\n");
4369
4370 let result = bash.exec("cat /config/app.conf").await.unwrap();
4371 assert_eq!(result.stdout, "name=${APP_NAME}");
4372 }
4373
4374 #[tokio::test]
4375 async fn test_text_file_json() {
4376 let mut bash = Bash::builder()
4377 .mount_text("/data/users.json", r#"["alice", "bob", "charlie"]"#)
4378 .build();
4379
4380 let result = bash.exec("cat /data/users.json | jq '.[0]'").await.unwrap();
4381 assert_eq!(result.stdout, "\"alice\"\n");
4382 }
4383
4384 #[tokio::test]
4385 async fn test_mount_with_custom_filesystem() {
4386 let custom_fs = std::sync::Arc::new(InMemoryFs::new());
4388
4389 custom_fs
4391 .write_file(std::path::Path::new("/base.txt"), b"from base")
4392 .await
4393 .unwrap();
4394
4395 let mut bash = Bash::builder()
4396 .fs(custom_fs)
4397 .mount_text("/mounted.txt", "from mount")
4398 .mount_readonly_text("/readonly.txt", "immutable")
4399 .build();
4400
4401 let result = bash.exec("cat /base.txt").await.unwrap();
4403 assert_eq!(result.stdout, "from base");
4404
4405 let result = bash.exec("cat /mounted.txt").await.unwrap();
4407 assert_eq!(result.stdout, "from mount");
4408
4409 let result = bash.exec("cat /readonly.txt").await.unwrap();
4410 assert_eq!(result.stdout, "immutable");
4411
4412 let stat = bash
4414 .fs()
4415 .stat(std::path::Path::new("/readonly.txt"))
4416 .await
4417 .unwrap();
4418 assert_eq!(stat.mode, 0o444);
4419 }
4420
4421 #[tokio::test]
4422 async fn test_mount_overwrites_base_file() {
4423 let custom_fs = std::sync::Arc::new(InMemoryFs::new());
4425 custom_fs
4426 .write_file(std::path::Path::new("/config.txt"), b"original")
4427 .await
4428 .unwrap();
4429
4430 let mut bash = Bash::builder()
4431 .fs(custom_fs)
4432 .mount_text("/config.txt", "overwritten")
4433 .build();
4434
4435 let result = bash.exec("cat /config.txt").await.unwrap();
4436 assert_eq!(result.stdout, "overwritten");
4437 }
4438
4439 #[tokio::test]
4444 async fn test_parse_error_includes_line_number() {
4445 let mut bash = Bash::new();
4447 let result = bash
4448 .exec(
4449 r#"echo ok
4450if true; then
4451echo missing fi"#,
4452 )
4453 .await;
4454 assert!(result.is_err());
4456 let err = result.unwrap_err();
4457 let err_msg = format!("{}", err);
4458 assert!(
4460 err_msg.contains("line") || err_msg.contains("parse"),
4461 "Error should be a parse error: {}",
4462 err_msg
4463 );
4464 }
4465
4466 #[tokio::test]
4467 async fn test_parse_error_on_specific_line() {
4468 use crate::parser::Parser;
4470 let script = "echo line1\necho line2\nif true; then\n";
4471 let result = Parser::new(script).parse();
4472 assert!(result.is_err());
4473 let err = result.unwrap_err();
4474 let err_msg = format!("{}", err);
4475 assert!(
4477 err_msg.contains("expected") || err_msg.contains("syntax error"),
4478 "Error should be a parse error: {}",
4479 err_msg
4480 );
4481 }
4482
4483 #[tokio::test]
4486 async fn test_cd_to_root_and_ls() {
4487 let mut bash = Bash::new();
4489 let result = bash.exec("cd / && ls").await.unwrap();
4490 assert_eq!(
4491 result.exit_code, 0,
4492 "cd / && ls should succeed: {}",
4493 result.stderr
4494 );
4495 assert!(result.stdout.contains("tmp"), "Root should contain tmp");
4496 assert!(result.stdout.contains("home"), "Root should contain home");
4497 }
4498
4499 #[tokio::test]
4500 async fn test_cd_to_root_and_pwd() {
4501 let mut bash = Bash::new();
4503 let result = bash.exec("cd / && pwd").await.unwrap();
4504 assert_eq!(result.exit_code, 0, "cd / && pwd should succeed");
4505 assert_eq!(result.stdout.trim(), "/");
4506 }
4507
4508 #[tokio::test]
4509 async fn test_cd_to_root_and_ls_dot() {
4510 let mut bash = Bash::new();
4512 let result = bash.exec("cd / && ls .").await.unwrap();
4513 assert_eq!(
4514 result.exit_code, 0,
4515 "cd / && ls . should succeed: {}",
4516 result.stderr
4517 );
4518 assert!(result.stdout.contains("tmp"), "Root should contain tmp");
4519 assert!(result.stdout.contains("home"), "Root should contain home");
4520 }
4521
4522 #[tokio::test]
4523 async fn test_ls_root_directly() {
4524 let mut bash = Bash::new();
4526 let result = bash.exec("ls /").await.unwrap();
4527 assert_eq!(
4528 result.exit_code, 0,
4529 "ls / should succeed: {}",
4530 result.stderr
4531 );
4532 assert!(result.stdout.contains("tmp"), "Root should contain tmp");
4533 assert!(result.stdout.contains("home"), "Root should contain home");
4534 assert!(result.stdout.contains("dev"), "Root should contain dev");
4535 }
4536
4537 #[tokio::test]
4538 async fn test_ls_root_long_format() {
4539 let mut bash = Bash::new();
4541 let result = bash.exec("ls -la /").await.unwrap();
4542 assert_eq!(
4543 result.exit_code, 0,
4544 "ls -la / should succeed: {}",
4545 result.stderr
4546 );
4547 assert!(result.stdout.contains("tmp"), "Root should contain tmp");
4548 assert!(
4549 result.stdout.contains("drw"),
4550 "Should show directory permissions"
4551 );
4552 }
4553
4554 #[tokio::test]
4557 async fn test_heredoc_redirect_to_file() {
4558 let mut bash = Bash::new();
4560 let result = bash
4561 .exec("cat > /tmp/out.txt <<'EOF'\nhello\nworld\nEOF\ncat /tmp/out.txt")
4562 .await
4563 .unwrap();
4564 assert_eq!(result.stdout, "hello\nworld\n");
4565 assert_eq!(result.exit_code, 0);
4566 }
4567
4568 #[tokio::test]
4569 async fn test_heredoc_redirect_to_file_unquoted() {
4570 let mut bash = Bash::new();
4571 let result = bash
4572 .exec("cat > /tmp/out.txt <<EOF\nhello\nworld\nEOF\ncat /tmp/out.txt")
4573 .await
4574 .unwrap();
4575 assert_eq!(result.stdout, "hello\nworld\n");
4576 assert_eq!(result.exit_code, 0);
4577 }
4578
4579 #[tokio::test]
4582 async fn test_pipe_to_while_read() {
4583 let mut bash = Bash::new();
4585 let result = bash
4586 .exec("echo -e 'a\\nb\\nc' | while read line; do echo \"got: $line\"; done")
4587 .await
4588 .unwrap();
4589 assert!(
4590 result.stdout.contains("got: a"),
4591 "stdout: {}",
4592 result.stdout
4593 );
4594 assert!(
4595 result.stdout.contains("got: b"),
4596 "stdout: {}",
4597 result.stdout
4598 );
4599 assert!(
4600 result.stdout.contains("got: c"),
4601 "stdout: {}",
4602 result.stdout
4603 );
4604 }
4605
4606 #[tokio::test]
4607 async fn test_pipe_to_while_read_count() {
4608 let mut bash = Bash::new();
4609 let result = bash
4610 .exec("printf 'x\\ny\\nz\\n' | while read line; do echo $line; done")
4611 .await
4612 .unwrap();
4613 assert_eq!(result.stdout, "x\ny\nz\n");
4614 }
4615
4616 #[tokio::test]
4619 async fn test_source_loads_functions() {
4620 let mut bash = Bash::new();
4621 bash.exec("cat > /tmp/lib.sh <<'EOF'\ngreet() { echo \"hello $1\"; }\nEOF")
4623 .await
4624 .unwrap();
4625 let result = bash.exec("source /tmp/lib.sh; greet world").await.unwrap();
4626 assert_eq!(result.stdout, "hello world\n");
4627 assert_eq!(result.exit_code, 0);
4628 }
4629
4630 #[tokio::test]
4631 async fn test_source_loads_variables() {
4632 let mut bash = Bash::new();
4633 bash.exec("echo 'MY_VAR=loaded' > /tmp/vars.sh")
4634 .await
4635 .unwrap();
4636 let result = bash
4637 .exec("source /tmp/vars.sh; echo $MY_VAR")
4638 .await
4639 .unwrap();
4640 assert_eq!(result.stdout, "loaded\n");
4641 }
4642
4643 #[tokio::test]
4646 async fn test_chmod_symbolic_plus_x() {
4647 let mut bash = Bash::new();
4648 bash.exec("echo '#!/bin/bash' > /tmp/script.sh")
4649 .await
4650 .unwrap();
4651 let result = bash.exec("chmod +x /tmp/script.sh").await.unwrap();
4652 assert_eq!(
4653 result.exit_code, 0,
4654 "chmod +x should succeed: {}",
4655 result.stderr
4656 );
4657 }
4658
4659 #[tokio::test]
4660 async fn test_chmod_symbolic_u_plus_x() {
4661 let mut bash = Bash::new();
4662 bash.exec("echo 'test' > /tmp/file.txt").await.unwrap();
4663 let result = bash.exec("chmod u+x /tmp/file.txt").await.unwrap();
4664 assert_eq!(
4665 result.exit_code, 0,
4666 "chmod u+x should succeed: {}",
4667 result.stderr
4668 );
4669 }
4670
4671 #[tokio::test]
4672 async fn test_chmod_symbolic_a_plus_r() {
4673 let mut bash = Bash::new();
4674 bash.exec("echo 'test' > /tmp/file.txt").await.unwrap();
4675 let result = bash.exec("chmod a+r /tmp/file.txt").await.unwrap();
4676 assert_eq!(
4677 result.exit_code, 0,
4678 "chmod a+r should succeed: {}",
4679 result.stderr
4680 );
4681 }
4682
4683 #[tokio::test]
4686 async fn test_awk_array_length() {
4687 let mut bash = Bash::new();
4689 let result = bash
4690 .exec(r#"echo "" | awk 'BEGIN{a[1]="x"; a[2]="y"; a[3]="z"} END{print length(a)}'"#)
4691 .await
4692 .unwrap();
4693 assert_eq!(result.stdout, "3\n");
4694 }
4695
4696 #[tokio::test]
4697 async fn test_awk_array_read_after_split() {
4698 let mut bash = Bash::new();
4700 let result = bash
4701 .exec(r#"echo "a:b:c" | awk '{n=split($0,arr,":"); for(i=1;i<=n;i++) print arr[i]}'"#)
4702 .await
4703 .unwrap();
4704 assert_eq!(result.stdout, "a\nb\nc\n");
4705 }
4706
4707 #[tokio::test]
4708 async fn test_awk_array_word_count_pattern() {
4709 let mut bash = Bash::new();
4711 let result = bash
4712 .exec(
4713 r#"printf "apple\nbanana\napple\ncherry\nbanana\napple" | awk '{count[$1]++} END{for(w in count) print w, count[w]}'"#,
4714 )
4715 .await
4716 .unwrap();
4717 assert!(
4718 result.stdout.contains("apple 3"),
4719 "stdout: {}",
4720 result.stdout
4721 );
4722 assert!(
4723 result.stdout.contains("banana 2"),
4724 "stdout: {}",
4725 result.stdout
4726 );
4727 assert!(
4728 result.stdout.contains("cherry 1"),
4729 "stdout: {}",
4730 result.stdout
4731 );
4732 }
4733
4734 #[tokio::test]
4737 async fn test_exec_streaming_for_loop() {
4738 let chunks = Arc::new(Mutex::new(Vec::new()));
4739 let chunks_cb = chunks.clone();
4740 let mut bash = Bash::new();
4741
4742 let result = bash
4743 .exec_streaming(
4744 "for i in 1 2 3; do echo $i; done",
4745 Box::new(move |stdout, _stderr| {
4746 chunks_cb.lock().unwrap().push(stdout.to_string());
4747 }),
4748 )
4749 .await
4750 .unwrap();
4751
4752 assert_eq!(result.stdout, "1\n2\n3\n");
4753 assert_eq!(
4754 *chunks.lock().unwrap(),
4755 vec!["1\n", "2\n", "3\n"],
4756 "each loop iteration should stream separately"
4757 );
4758 }
4759
4760 #[tokio::test]
4761 async fn test_exec_streaming_while_loop() {
4762 let chunks = Arc::new(Mutex::new(Vec::new()));
4763 let chunks_cb = chunks.clone();
4764 let mut bash = Bash::new();
4765
4766 let result = bash
4767 .exec_streaming(
4768 "i=0; while [ $i -lt 3 ]; do i=$((i+1)); echo $i; done",
4769 Box::new(move |stdout, _stderr| {
4770 chunks_cb.lock().unwrap().push(stdout.to_string());
4771 }),
4772 )
4773 .await
4774 .unwrap();
4775
4776 assert_eq!(result.stdout, "1\n2\n3\n");
4777 let chunks = chunks.lock().unwrap();
4778 assert!(
4780 chunks.contains(&"1\n".to_string()),
4781 "should contain first iteration output"
4782 );
4783 assert!(
4784 chunks.contains(&"2\n".to_string()),
4785 "should contain second iteration output"
4786 );
4787 assert!(
4788 chunks.contains(&"3\n".to_string()),
4789 "should contain third iteration output"
4790 );
4791 }
4792
4793 #[tokio::test]
4794 async fn test_exec_streaming_no_callback_still_works() {
4795 let mut bash = Bash::new();
4797 let result = bash.exec("for i in a b c; do echo $i; done").await.unwrap();
4798 assert_eq!(result.stdout, "a\nb\nc\n");
4799 }
4800
4801 #[tokio::test]
4802 async fn test_exec_streaming_nested_loops_no_duplicates() {
4803 let chunks = Arc::new(Mutex::new(Vec::new()));
4804 let chunks_cb = chunks.clone();
4805 let mut bash = Bash::new();
4806
4807 let result = bash
4808 .exec_streaming(
4809 "for i in 1 2; do for j in a b; do echo \"$i$j\"; done; done",
4810 Box::new(move |stdout, _stderr| {
4811 chunks_cb.lock().unwrap().push(stdout.to_string());
4812 }),
4813 )
4814 .await
4815 .unwrap();
4816
4817 assert_eq!(result.stdout, "1a\n1b\n2a\n2b\n");
4818 let chunks = chunks.lock().unwrap();
4819 let total_chars: usize = chunks.iter().map(|c| c.len()).sum();
4821 assert_eq!(
4822 total_chars,
4823 result.stdout.len(),
4824 "total streamed bytes should match final output: chunks={:?}",
4825 *chunks
4826 );
4827 }
4828
4829 #[tokio::test]
4830 async fn test_exec_streaming_mixed_list_and_loop() {
4831 let chunks = Arc::new(Mutex::new(Vec::new()));
4832 let chunks_cb = chunks.clone();
4833 let mut bash = Bash::new();
4834
4835 let result = bash
4836 .exec_streaming(
4837 "echo start; for i in 1 2; do echo $i; done; echo end",
4838 Box::new(move |stdout, _stderr| {
4839 chunks_cb.lock().unwrap().push(stdout.to_string());
4840 }),
4841 )
4842 .await
4843 .unwrap();
4844
4845 assert_eq!(result.stdout, "start\n1\n2\nend\n");
4846 let chunks = chunks.lock().unwrap();
4847 assert_eq!(
4848 *chunks,
4849 vec!["start\n", "1\n", "2\n", "end\n"],
4850 "mixed list+loop should produce exactly 4 events"
4851 );
4852 }
4853
4854 #[tokio::test]
4855 async fn test_exec_streaming_stderr() {
4856 let stderr_chunks = Arc::new(Mutex::new(Vec::new()));
4857 let stderr_cb = stderr_chunks.clone();
4858 let mut bash = Bash::new();
4859
4860 let result = bash
4861 .exec_streaming(
4862 "echo ok; echo err >&2; echo ok2",
4863 Box::new(move |_stdout, stderr| {
4864 if !stderr.is_empty() {
4865 stderr_cb.lock().unwrap().push(stderr.to_string());
4866 }
4867 }),
4868 )
4869 .await
4870 .unwrap();
4871
4872 assert_eq!(result.stdout, "ok\nok2\n");
4873 assert_eq!(result.stderr, "err\n");
4874 let stderr_chunks = stderr_chunks.lock().unwrap();
4875 assert!(
4876 stderr_chunks.contains(&"err\n".to_string()),
4877 "stderr should be streamed: {:?}",
4878 *stderr_chunks
4879 );
4880 }
4881
4882 async fn assert_streaming_equivalence(script: &str) {
4889 let mut bash_plain = Bash::new();
4891 let plain = bash_plain.exec(script).await.unwrap();
4892
4893 let stdout_chunks: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
4895 let stderr_chunks: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
4896 let so = stdout_chunks.clone();
4897 let se = stderr_chunks.clone();
4898 let mut bash_stream = Bash::new();
4899 let streamed = bash_stream
4900 .exec_streaming(
4901 script,
4902 Box::new(move |stdout, stderr| {
4903 if !stdout.is_empty() {
4904 so.lock().unwrap().push(stdout.to_string());
4905 }
4906 if !stderr.is_empty() {
4907 se.lock().unwrap().push(stderr.to_string());
4908 }
4909 }),
4910 )
4911 .await
4912 .unwrap();
4913
4914 assert_eq!(
4916 plain.stdout, streamed.stdout,
4917 "stdout mismatch for: {script}"
4918 );
4919 assert_eq!(
4920 plain.stderr, streamed.stderr,
4921 "stderr mismatch for: {script}"
4922 );
4923 assert_eq!(
4924 plain.exit_code, streamed.exit_code,
4925 "exit_code mismatch for: {script}"
4926 );
4927
4928 let reassembled_stdout: String = stdout_chunks.lock().unwrap().iter().cloned().collect();
4930 assert_eq!(
4931 reassembled_stdout, streamed.stdout,
4932 "reassembled stdout chunks != final stdout for: {script}"
4933 );
4934 let reassembled_stderr: String = stderr_chunks.lock().unwrap().iter().cloned().collect();
4935 assert_eq!(
4936 reassembled_stderr, streamed.stderr,
4937 "reassembled stderr chunks != final stderr for: {script}"
4938 );
4939 }
4940
4941 #[tokio::test]
4942 async fn test_streaming_equivalence_for_loop() {
4943 assert_streaming_equivalence("for i in 1 2 3; do echo $i; done").await;
4944 }
4945
4946 #[tokio::test]
4947 async fn test_streaming_equivalence_while_loop() {
4948 assert_streaming_equivalence("i=0; while [ $i -lt 4 ]; do i=$((i+1)); echo $i; done").await;
4949 }
4950
4951 #[tokio::test]
4952 async fn test_streaming_equivalence_nested_loops() {
4953 assert_streaming_equivalence("for i in a b; do for j in 1 2; do echo \"$i$j\"; done; done")
4954 .await;
4955 }
4956
4957 #[tokio::test]
4958 async fn test_streaming_equivalence_mixed_list() {
4959 assert_streaming_equivalence("echo start; for i in x y; do echo $i; done; echo end").await;
4960 }
4961
4962 #[tokio::test]
4963 async fn test_streaming_equivalence_stderr() {
4964 assert_streaming_equivalence("echo out; echo err >&2; echo out2").await;
4965 }
4966
4967 #[tokio::test]
4968 async fn test_streaming_equivalence_pipeline() {
4969 assert_streaming_equivalence("echo -e 'a\\nb\\nc' | grep b").await;
4970 }
4971
4972 #[tokio::test]
4973 async fn test_streaming_equivalence_conditionals() {
4974 assert_streaming_equivalence("if true; then echo yes; else echo no; fi; echo done").await;
4975 }
4976
4977 #[tokio::test]
4978 async fn test_streaming_equivalence_subshell() {
4979 assert_streaming_equivalence("x=$(echo hello); echo $x").await;
4980 }
4981}