Skip to content

Commit 8c03e30

Browse files
authored
fix(interpreter): route exec fd redirects through VFS targets (#1034)
## Summary - Add persistent `exec_fd_table` to track fd output targets set by `exec N>/path` - `exec N>/dev/null` now properly discards writes to fd N - `exec N>/file` routes writes to the VFS file - `exec N>&M` duplicates fd targets; `exec N>&-` closes them - Both `apply_redirections` and `apply_redirections_fd_table` check exec_fd_table ## Test plan - [x] Unit tests: `test_exec_fd_to_dev_null`, `test_exec_fd_to_file` - [x] Spec tests: `exec-fd-redirect.test.sh` (4 cases: dev/null, file, dup, close) - [x] Full test suite passes (2222 tests) Closes #938
1 parent 5f72ac0 commit 8c03e30

File tree

2 files changed

+123
-12
lines changed

2 files changed

+123
-12
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,9 @@ pub struct Interpreter {
475475
coproc_buffers: HashMap<i32, Vec<String>>,
476476
/// Next virtual FD to assign for coproc read ends (starts at 63, like bash).
477477
coproc_next_fd: i32,
478+
/// Persistent fd output table set by `exec N>/path` redirections.
479+
/// Maps fd number to its output target. Used by `>&N` redirections.
480+
exec_fd_table: HashMap<i32, FdTarget>,
478481
/// Cancellation token: when set to `true`, execution aborts at the next
479482
/// command boundary with `Error::Cancelled`.
480483
cancelled: Arc<AtomicBool>,
@@ -783,6 +786,7 @@ impl Interpreter {
783786
subst_generation: 0,
784787
coproc_buffers: HashMap::new(),
785788
coproc_next_fd: 63,
789+
exec_fd_table: HashMap::new(),
786790
cancelled: Arc::new(AtomicBool::new(false)),
787791
deferred_proc_subs: Vec::new(),
788792
}
@@ -3668,6 +3672,51 @@ impl Interpreter {
36683672
self.coproc_buffers.remove(&fd);
36693673
}
36703674
}
3675+
RedirectKind::Output | RedirectKind::Clobber => {
3676+
let fd = redirect.fd.unwrap_or(1);
3677+
let target_path = self.expand_word(&redirect.target).await?;
3678+
let path = self.resolve_path(&target_path);
3679+
if is_dev_null(&path) {
3680+
self.exec_fd_table.insert(fd, FdTarget::DevNull);
3681+
} else {
3682+
// Truncate file on open (like real exec >file)
3683+
let _ = self.fs.write_file(&path, b"").await;
3684+
self.exec_fd_table
3685+
.insert(fd, FdTarget::WriteFile(path, target_path));
3686+
}
3687+
}
3688+
RedirectKind::Append => {
3689+
let fd = redirect.fd.unwrap_or(1);
3690+
let target_path = self.expand_word(&redirect.target).await?;
3691+
let path = self.resolve_path(&target_path);
3692+
if is_dev_null(&path) {
3693+
self.exec_fd_table.insert(fd, FdTarget::DevNull);
3694+
} else {
3695+
self.exec_fd_table
3696+
.insert(fd, FdTarget::AppendFile(path, target_path));
3697+
}
3698+
}
3699+
RedirectKind::DupOutput => {
3700+
let target = self.expand_word(&redirect.target).await?;
3701+
let fd = redirect.fd.unwrap_or(1);
3702+
if target == "-" {
3703+
// exec N>&- closes the fd
3704+
self.exec_fd_table.remove(&fd);
3705+
} else if let Ok(target_fd) = target.parse::<i32>() {
3706+
// exec N>&M duplicates fd M to fd N
3707+
let target_entry = if target_fd == 1 {
3708+
FdTarget::Stdout
3709+
} else if target_fd == 2 {
3710+
FdTarget::Stderr
3711+
} else {
3712+
self.exec_fd_table
3713+
.get(&target_fd)
3714+
.cloned()
3715+
.unwrap_or(FdTarget::Stdout)
3716+
};
3717+
self.exec_fd_table.insert(fd, target_entry);
3718+
}
3719+
}
36713720
_ => {}
36723721
}
36733722
}
@@ -5625,16 +5674,33 @@ impl Interpreter {
56255674
let target_fd: i32 = target.parse().unwrap_or(1);
56265675
let src_fd = redirect.fd.unwrap_or(1);
56275676

5628-
match (src_fd, target_fd) {
5629-
(2, 1) => {
5630-
result.stdout.push_str(&result.stderr);
5631-
result.stderr = String::new();
5677+
// Check exec_fd_table for persistent fd targets
5678+
if let Some(fd_target) = self.exec_fd_table.get(&target_fd).cloned() {
5679+
let data = if src_fd == 2 {
5680+
std::mem::take(&mut result.stderr)
5681+
} else {
5682+
std::mem::take(&mut result.stdout)
5683+
};
5684+
match &fd_target {
5685+
FdTarget::Stdout => result.stdout.push_str(&data),
5686+
FdTarget::Stderr => result.stderr.push_str(&data),
5687+
FdTarget::DevNull => {}
5688+
FdTarget::WriteFile(path, _) | FdTarget::AppendFile(path, _) => {
5689+
self.fs.append_file(path, data.as_bytes()).await?;
5690+
}
56325691
}
5633-
(1, 2) => {
5634-
result.stderr.push_str(&result.stdout);
5635-
result.stdout = String::new();
5692+
} else {
5693+
match (src_fd, target_fd) {
5694+
(2, 1) => {
5695+
result.stdout.push_str(&result.stderr);
5696+
result.stderr = String::new();
5697+
}
5698+
(1, 2) => {
5699+
result.stderr.push_str(&result.stdout);
5700+
result.stdout = String::new();
5701+
}
5702+
_ => {}
56365703
}
5637-
_ => {}
56385704
}
56395705
}
56405706
RedirectKind::Input
@@ -5716,10 +5782,18 @@ impl Interpreter {
57165782
let target_fd: i32 = target.parse().unwrap_or(1);
57175783
let src_fd = redirect.fd.unwrap_or(1);
57185784

5719-
match (src_fd, target_fd) {
5720-
(2, 1) => fd2 = fd1.clone(),
5721-
(1, 2) => fd1 = fd2.clone(),
5722-
_ => {}
5785+
// Look up exec_fd_table for persistent fd targets
5786+
if let Some(exec_target) = self.exec_fd_table.get(&target_fd).cloned() {
5787+
match src_fd {
5788+
2 => fd2 = exec_target,
5789+
_ => fd1 = exec_target,
5790+
}
5791+
} else {
5792+
match (src_fd, target_fd) {
5793+
(2, 1) => fd2 = fd1.clone(),
5794+
(1, 2) => fd1 = fd2.clone(),
5795+
_ => {}
5796+
}
57235797
}
57245798
}
57255799
RedirectKind::Input
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
### exec_fd_to_dev_null
2+
# exec N>/dev/null should discard writes to fd N
3+
exec 3>/dev/null
4+
echo "discarded" >&3
5+
exec 3>&-
6+
echo "visible"
7+
### expect
8+
visible
9+
### end
10+
11+
### exec_fd_to_file
12+
# exec N>file should redirect writes to fd N into file
13+
exec 3>/tmp/fd_test_out.txt
14+
echo "captured" >&3
15+
exec 3>&-
16+
cat /tmp/fd_test_out.txt
17+
### expect
18+
captured
19+
### end
20+
21+
### exec_fd_dup_stdout
22+
# exec 3>&1 should duplicate stdout to fd 3
23+
exec 3>&1
24+
echo "on fd3" >&3
25+
exec 3>&-
26+
### expect
27+
on fd3
28+
### end
29+
30+
### exec_fd_close
31+
# exec 3>&- should close fd 3
32+
exec 3>/dev/null
33+
exec 3>&-
34+
echo "closed ok"
35+
### expect
36+
closed ok
37+
### end

0 commit comments

Comments
 (0)