Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions crates/bashkit/src/builtins/test.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! test builtin command ([ and test)

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

Expand All @@ -23,7 +24,7 @@ impl Builtin for Test {

let cwd = ctx.cwd.clone();
// Parse and evaluate the expression
let result = evaluate_expression(ctx.args, &ctx.fs, &cwd).await;
let result = evaluate_expression(ctx.args, &ctx.fs, &cwd, ctx.variables).await;

if result {
Ok(ExecResult::ok(String::new()))
Expand Down Expand Up @@ -54,7 +55,7 @@ impl Builtin for Bracket {

let cwd = ctx.cwd.clone();
// Parse and evaluate the expression
let result = evaluate_expression(&args, &ctx.fs, &cwd).await;
let result = evaluate_expression(&args, &ctx.fs, &cwd, ctx.variables).await;

if result {
Ok(ExecResult::ok(String::new()))
Expand All @@ -79,6 +80,7 @@ fn evaluate_expression<'a>(
args: &'a [String],
fs: &'a Arc<dyn FileSystem>,
cwd: &'a Path,
variables: &'a HashMap<String, String>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + 'a>> {
Box::pin(async move {
if args.is_empty() {
Expand All @@ -87,26 +89,26 @@ fn evaluate_expression<'a>(

// Handle negation
if args[0] == "!" {
return !evaluate_expression(&args[1..], fs, cwd).await;
return !evaluate_expression(&args[1..], fs, cwd, variables).await;
}

// Handle parentheses (basic support)
if args[0] == "(" && args.last().map(|s| s.as_str()) == Some(")") {
return evaluate_expression(&args[1..args.len() - 1], fs, cwd).await;
return evaluate_expression(&args[1..args.len() - 1], fs, cwd, variables).await;
}

// Look for logical operators: -o has lowest precedence, then -a.
// Scan for -o first (split at lowest precedence first).
for (i, arg) in args.iter().enumerate() {
if arg == "-o" && i > 0 {
return evaluate_expression(&args[..i], fs, cwd).await
|| evaluate_expression(&args[i + 1..], fs, cwd).await;
return evaluate_expression(&args[..i], fs, cwd, variables).await
|| evaluate_expression(&args[i + 1..], fs, cwd, variables).await;
}
}
for (i, arg) in args.iter().enumerate() {
if arg == "-a" && i > 0 {
return evaluate_expression(&args[..i], fs, cwd).await
&& evaluate_expression(&args[i + 1..], fs, cwd).await;
return evaluate_expression(&args[..i], fs, cwd, variables).await
&& evaluate_expression(&args[i + 1..], fs, cwd, variables).await;
}
}

Expand All @@ -118,7 +120,7 @@ fn evaluate_expression<'a>(
}
2 => {
// Unary operators
evaluate_unary(&args[0], &args[1], fs, cwd).await
evaluate_unary(&args[0], &args[1], fs, cwd, variables).await
}
3 => {
// Binary operators
Expand All @@ -130,7 +132,13 @@ fn evaluate_expression<'a>(
}

/// Evaluate a unary test expression
async fn evaluate_unary(op: &str, arg: &str, fs: &Arc<dyn FileSystem>, cwd: &Path) -> bool {
async fn evaluate_unary(
op: &str,
arg: &str,
fs: &Arc<dyn FileSystem>,
cwd: &Path,
variables: &HashMap<String, String>,
) -> bool {
match op {
// String tests
"-z" => arg.is_empty(),
Expand Down Expand Up @@ -211,7 +219,13 @@ async fn evaluate_unary(op: &str, arg: &str, fs: &Arc<dyn FileSystem>, cwd: &Pat
"-S" => false, // socket (not supported)
"-b" => false, // block device (not supported)
"-c" => false, // character device (not supported)
"-t" => false, // file descriptor is open and refers to a terminal (not supported)
"-t" => {
// file descriptor refers to a terminal
// In VFS sandbox, defaults to false for all FDs.
// Configurable via _TTY_N variables (e.g. _TTY_0=1 for stdin).
let fd_key = format!("_TTY_{}", arg);
variables.get(&fd_key).map(|v| v == "1").unwrap_or(false)
}

_ => false,
}
Expand Down
8 changes: 8 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1926,6 +1926,14 @@ impl Interpreter {
.await
.map(|m| m.size > 0)
.unwrap_or(false),
"-t" => {
// fd is a terminal — configurable via _TTY_N variables
let fd_key = format!("_TTY_{}", args[1]);
self.variables
.get(&fd_key)
.map(|v| v == "1")
.unwrap_or(false)
}
_ => !args[0].is_empty(),
}
}
Expand Down
58 changes: 58 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/test-tty.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
### test_t_default_false
# -t 0 defaults to false in VFS sandbox
if [ -t 0 ]; then
echo "terminal"
else
echo "not terminal"
fi
### expect
not terminal
### end

### test_t_stdout_false
# -t 1 defaults to false
if [ -t 1 ]; then
echo "terminal"
else
echo "not terminal"
fi
### expect
not terminal
### end

### test_t_configurable
### bash_diff: _TTY_N is a bashkit-specific extension for configuring terminal state
# _TTY_1=1 makes -t 1 return true
_TTY_1=1
if [ -t 1 ]; then
echo "terminal"
else
echo "not terminal"
fi
### expect
terminal
### end

### test_t_conditional_syntax
# [[ -t 0 ]] also works
if [[ -t 0 ]]; then
echo "terminal"
else
echo "not terminal"
fi
### expect
not terminal
### end

### test_t_conditional_configurable
### bash_diff: _TTY_N is a bashkit-specific extension for configuring terminal state
# [[ -t 1 ]] respects _TTY_1 variable
_TTY_1=1
if [[ -t 1 ]]; then
echo "terminal"
else
echo "not terminal"
fi
### expect
terminal
### end
Loading