Skip to content

Commit bf6edc6

Browse files
committed
feat(test): configurable -t fd terminal detection
Make -t fd (terminal test) configurable via _TTY_N variables. Defaults to false for all FDs (correct for sandboxed VFS execution). Set _TTY_0=1, _TTY_1=1, etc. to simulate terminal mode. Works in both [ -t ] and [[ -t ]] syntax. Closes #799
1 parent 267d594 commit bf6edc6

File tree

3 files changed

+91
-11
lines changed

3 files changed

+91
-11
lines changed

crates/bashkit/src/builtins/test.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! test builtin command ([ and test)
22
3+
use std::collections::HashMap;
34
use std::path::{Path, PathBuf};
45
use std::sync::Arc;
56

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

2425
let cwd = ctx.cwd.clone();
2526
// Parse and evaluate the expression
26-
let result = evaluate_expression(ctx.args, &ctx.fs, &cwd).await;
27+
let result = evaluate_expression(ctx.args, &ctx.fs, &cwd, ctx.variables).await;
2728

2829
if result {
2930
Ok(ExecResult::ok(String::new()))
@@ -54,7 +55,7 @@ impl Builtin for Bracket {
5455

5556
let cwd = ctx.cwd.clone();
5657
// Parse and evaluate the expression
57-
let result = evaluate_expression(&args, &ctx.fs, &cwd).await;
58+
let result = evaluate_expression(&args, &ctx.fs, &cwd, ctx.variables).await;
5859

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

8890
// Handle negation
8991
if args[0] == "!" {
90-
return !evaluate_expression(&args[1..], fs, cwd).await;
92+
return !evaluate_expression(&args[1..], fs, cwd, variables).await;
9193
}
9294

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

98100
// Look for logical operators: -o has lowest precedence, then -a.
99101
// Scan for -o first (split at lowest precedence first).
100102
for (i, arg) in args.iter().enumerate() {
101103
if arg == "-o" && i > 0 {
102-
return evaluate_expression(&args[..i], fs, cwd).await
103-
|| evaluate_expression(&args[i + 1..], fs, cwd).await;
104+
return evaluate_expression(&args[..i], fs, cwd, variables).await
105+
|| evaluate_expression(&args[i + 1..], fs, cwd, variables).await;
104106
}
105107
}
106108
for (i, arg) in args.iter().enumerate() {
107109
if arg == "-a" && i > 0 {
108-
return evaluate_expression(&args[..i], fs, cwd).await
109-
&& evaluate_expression(&args[i + 1..], fs, cwd).await;
110+
return evaluate_expression(&args[..i], fs, cwd, variables).await
111+
&& evaluate_expression(&args[i + 1..], fs, cwd, variables).await;
110112
}
111113
}
112114

@@ -118,7 +120,7 @@ fn evaluate_expression<'a>(
118120
}
119121
2 => {
120122
// Unary operators
121-
evaluate_unary(&args[0], &args[1], fs, cwd).await
123+
evaluate_unary(&args[0], &args[1], fs, cwd, variables).await
122124
}
123125
3 => {
124126
// Binary operators
@@ -130,7 +132,13 @@ fn evaluate_expression<'a>(
130132
}
131133

132134
/// Evaluate a unary test expression
133-
async fn evaluate_unary(op: &str, arg: &str, fs: &Arc<dyn FileSystem>, cwd: &Path) -> bool {
135+
async fn evaluate_unary(
136+
op: &str,
137+
arg: &str,
138+
fs: &Arc<dyn FileSystem>,
139+
cwd: &Path,
140+
variables: &HashMap<String, String>,
141+
) -> bool {
134142
match op {
135143
// String tests
136144
"-z" => arg.is_empty(),
@@ -211,7 +219,13 @@ async fn evaluate_unary(op: &str, arg: &str, fs: &Arc<dyn FileSystem>, cwd: &Pat
211219
"-S" => false, // socket (not supported)
212220
"-b" => false, // block device (not supported)
213221
"-c" => false, // character device (not supported)
214-
"-t" => false, // file descriptor is open and refers to a terminal (not supported)
222+
"-t" => {
223+
// file descriptor refers to a terminal
224+
// In VFS sandbox, defaults to false for all FDs.
225+
// Configurable via _TTY_N variables (e.g. _TTY_0=1 for stdin).
226+
let fd_key = format!("_TTY_{}", arg);
227+
variables.get(&fd_key).map(|v| v == "1").unwrap_or(false)
228+
}
215229

216230
_ => false,
217231
}

crates/bashkit/src/interpreter/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,14 @@ impl Interpreter {
19261926
.await
19271927
.map(|m| m.size > 0)
19281928
.unwrap_or(false),
1929+
"-t" => {
1930+
// fd is a terminal — configurable via _TTY_N variables
1931+
let fd_key = format!("_TTY_{}", args[1]);
1932+
self.variables
1933+
.get(&fd_key)
1934+
.map(|v| v == "1")
1935+
.unwrap_or(false)
1936+
}
19291937
_ => !args[0].is_empty(),
19301938
}
19311939
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
### test_t_default_false
2+
# -t 0 defaults to false in VFS sandbox
3+
if [ -t 0 ]; then
4+
echo "terminal"
5+
else
6+
echo "not terminal"
7+
fi
8+
### expect
9+
not terminal
10+
### end
11+
12+
### test_t_stdout_false
13+
# -t 1 defaults to false
14+
if [ -t 1 ]; then
15+
echo "terminal"
16+
else
17+
echo "not terminal"
18+
fi
19+
### expect
20+
not terminal
21+
### end
22+
23+
### test_t_configurable
24+
### bash_diff: _TTY_N is a bashkit-specific extension for configuring terminal state
25+
# _TTY_1=1 makes -t 1 return true
26+
_TTY_1=1
27+
if [ -t 1 ]; then
28+
echo "terminal"
29+
else
30+
echo "not terminal"
31+
fi
32+
### expect
33+
terminal
34+
### end
35+
36+
### test_t_conditional_syntax
37+
# [[ -t 0 ]] also works
38+
if [[ -t 0 ]]; then
39+
echo "terminal"
40+
else
41+
echo "not terminal"
42+
fi
43+
### expect
44+
not terminal
45+
### end
46+
47+
### test_t_conditional_configurable
48+
### bash_diff: _TTY_N is a bashkit-specific extension for configuring terminal state
49+
# [[ -t 1 ]] respects _TTY_1 variable
50+
_TTY_1=1
51+
if [[ -t 1 ]]; then
52+
echo "terminal"
53+
else
54+
echo "not terminal"
55+
fi
56+
### expect
57+
terminal
58+
### end

0 commit comments

Comments
 (0)