From 4707db9e6903135a5670c43854ffbaa99b9cfa4f Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 26 Mar 2026 14:21:15 +0000 Subject: [PATCH] feat(api): add BashBuilder::tty() for configurable terminal detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #799 — add tty(fd, is_terminal) method to BashBuilder that configures whether [ -t fd ] reports a file descriptor as a terminal. Defaults remain false (non-interactive) for all FDs in the sandbox. The internal _TTY_N variable mechanism already existed; this exposes it through the public builder API. --- crates/bashkit/src/lib.rs | 20 ++++++++++++++ crates/bashkit/tests/tty_tests.rs | 46 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 crates/bashkit/tests/tty_tests.rs diff --git a/crates/bashkit/src/lib.rs b/crates/bashkit/src/lib.rs index f552ae02..f3f1a767 100644 --- a/crates/bashkit/src/lib.rs +++ b/crates/bashkit/src/lib.rs @@ -1092,6 +1092,26 @@ impl BashBuilder { self } + /// Configure whether a file descriptor is reported as a terminal by `[ -t fd ]`. + /// + /// In a sandboxed VFS environment, all FDs default to non-terminal (false). + /// Use this to simulate interactive mode for scripts that check `[ -t 0 ]` + /// (stdin), `[ -t 1 ]` (stdout), or `[ -t 2 ]` (stderr). + /// + /// ```rust + /// # use bashkit::Bash; + /// let bash = Bash::builder() + /// .tty(0, true) // stdin is a terminal + /// .tty(1, true) // stdout is a terminal + /// .build(); + /// ``` + pub fn tty(mut self, fd: u32, is_terminal: bool) -> Self { + if is_terminal { + self.env.insert(format!("_TTY_{}", fd), "1".to_string()); + } + self + } + /// Set a fixed Unix epoch for the `date` builtin. /// /// THREAT[TM-INF-018]: Prevents `date` from leaking real host time. diff --git a/crates/bashkit/tests/tty_tests.rs b/crates/bashkit/tests/tty_tests.rs new file mode 100644 index 00000000..0b13a542 --- /dev/null +++ b/crates/bashkit/tests/tty_tests.rs @@ -0,0 +1,46 @@ +//! Tests for [[ -t fd ]] terminal detection + +use bashkit::Bash; + +/// Issue #799: -t defaults to false in sandbox +#[tokio::test] +async fn tty_defaults_to_false() { + let mut bash = Bash::new(); + let result = bash + .exec("[[ -t 0 ]] && echo yes || echo no") + .await + .unwrap(); + assert_eq!(result.stdout.trim(), "no"); +} + +/// -t can be configured via builder +#[tokio::test] +async fn tty_configurable_via_builder() { + let mut bash = Bash::builder().tty(0, true).tty(1, true).build(); + let result = bash + .exec("[[ -t 0 ]] && echo stdin_tty || echo stdin_no") + .await + .unwrap(); + assert_eq!(result.stdout.trim(), "stdin_tty"); + + let result = bash + .exec("[[ -t 1 ]] && echo stdout_tty || echo stdout_no") + .await + .unwrap(); + assert_eq!(result.stdout.trim(), "stdout_tty"); + + // fd 2 not configured, should be false + let result = bash + .exec("[[ -t 2 ]] && echo stderr_tty || echo stderr_no") + .await + .unwrap(); + assert_eq!(result.stdout.trim(), "stderr_no"); +} + +/// test builtin [ -t ] also works +#[tokio::test] +async fn tty_test_builtin_bracket() { + let mut bash = Bash::builder().tty(1, true).build(); + let result = bash.exec("[ -t 1 ] && echo yes || echo no").await.unwrap(); + assert_eq!(result.stdout.trim(), "yes"); +}