Skip to content

Commit 0b91a41

Browse files
authored
fix(interpreter): support exec {var}>&- fd-variable redirect syntax (#1060)
## Summary - Add parser support for `{varname}` fd-variable redirect syntax (e.g., `exec {myfd}>&-`) - Added `fd_var: Option<String>` to `Redirect` AST node - Parser detects `{identifier}` pattern before redirect operators via `pop_fd_var()` - Interpreter resolves fd variable from shell variables in `execute_exec_builtin` ## Test plan - [ ] `exec_fd_variable_close` — close fd via variable reference - [ ] `exec_fd_variable_open` — open fd via variable reference - [ ] All 1925 bash spec tests pass - [ ] Full test suite passes (2226+ tests) Closes #964
1 parent 7e31915 commit 0b91a41

File tree

4 files changed

+102
-7
lines changed

4 files changed

+102
-7
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3757,13 +3757,20 @@ impl Interpreter {
37573757
});
37583758
}
37593759
for redirect in redirects {
3760+
// Resolve fd from either explicit fd or {var} fd-variable syntax
3761+
let resolved_fd_var: Option<i32> = redirect.fd_var.as_ref().and_then(|var_name| {
3762+
self.variables
3763+
.get(var_name)
3764+
.and_then(|val| val.parse::<i32>().ok())
3765+
});
37603766
match redirect.kind {
37613767
RedirectKind::Input => {
37623768
let target_path = self.expand_word(&redirect.target).await?;
37633769
let path = self.resolve_path(&target_path);
37643770
let content = self.fs.read_file(&path).await?;
37653771
let text = bytes_to_latin1_string(&content);
3766-
if let Some(fd) = redirect.fd {
3772+
let fd = redirect.fd.or(resolved_fd_var);
3773+
if let Some(fd) = fd {
37673774
let lines: Vec<String> =
37683775
text.lines().rev().map(|l| l.to_string()).collect();
37693776
self.coproc_buffers.insert(fd, lines);
@@ -3774,14 +3781,15 @@ impl Interpreter {
37743781
}
37753782
RedirectKind::DupInput => {
37763783
let target = self.expand_word(&redirect.target).await?;
3784+
let fd = redirect.fd.or(resolved_fd_var);
37773785
if target == "-"
3778-
&& let Some(fd) = redirect.fd
3786+
&& let Some(fd) = fd
37793787
{
37803788
self.coproc_buffers.remove(&fd);
37813789
}
37823790
}
37833791
RedirectKind::Output | RedirectKind::Clobber => {
3784-
let fd = redirect.fd.unwrap_or(1);
3792+
let fd = redirect.fd.or(resolved_fd_var).unwrap_or(1);
37853793
let target_path = self.expand_word(&redirect.target).await?;
37863794
let path = self.resolve_path(&target_path);
37873795
if is_dev_null(&path) {
@@ -3794,7 +3802,7 @@ impl Interpreter {
37943802
}
37953803
}
37963804
RedirectKind::Append => {
3797-
let fd = redirect.fd.unwrap_or(1);
3805+
let fd = redirect.fd.or(resolved_fd_var).unwrap_or(1);
37983806
let target_path = self.expand_word(&redirect.target).await?;
37993807
let path = self.resolve_path(&target_path);
38003808
if is_dev_null(&path) {
@@ -3806,7 +3814,7 @@ impl Interpreter {
38063814
}
38073815
RedirectKind::DupOutput => {
38083816
let target = self.expand_word(&redirect.target).await?;
3809-
let fd = redirect.fd.unwrap_or(1);
3817+
let fd = redirect.fd.or(resolved_fd_var).unwrap_or(1);
38103818
if target == "-" {
38113819
// exec N>&- closes the fd
38123820
self.exec_fd_table.remove(&fd);
@@ -5522,6 +5530,7 @@ impl Interpreter {
55225530
let inner_redirects = if let Some(ref stdin_data) = command.stdin {
55235531
vec![Redirect {
55245532
fd: None,
5533+
fd_var: None,
55255534
kind: RedirectKind::HereString,
55265535
target: Word::literal(stdin_data.trim_end_matches('\n').to_string()),
55275536
}]
@@ -5565,6 +5574,7 @@ impl Interpreter {
55655574
let cmd_redirects = if let Some(ref stdin_data) = cmd.stdin {
55665575
vec![Redirect {
55675576
fd: None,
5577+
fd_var: None,
55685578
kind: RedirectKind::HereString,
55695579
target: Word::literal(stdin_data.trim_end_matches('\n').to_string()),
55705580
}]

crates/bashkit/src/parser/ast.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,8 @@ pub enum ParameterOp {
485485
pub struct Redirect {
486486
/// File descriptor (default: 1 for output, 0 for input)
487487
pub fd: Option<i32>,
488+
/// Variable name for `{var}` fd-variable redirects (e.g. `exec {myfd}>&-`)
489+
pub fd_var: Option<String>,
488490
/// Type of redirection
489491
pub kind: RedirectKind,
490492
/// Target (file, fd, or heredoc content)
@@ -921,6 +923,7 @@ mod tests {
921923
args: vec![Word::literal("hi")],
922924
redirects: vec![Redirect {
923925
fd: Some(1),
926+
fd_var: None,
924927
kind: RedirectKind::Output,
925928
target: Word::literal("out.txt"),
926929
}],
@@ -1049,6 +1052,7 @@ mod tests {
10491052
fn redirect_default_fd_none() {
10501053
let r = Redirect {
10511054
fd: None,
1055+
fd_var: None,
10521056
kind: RedirectKind::Input,
10531057
target: Word::literal("input.txt"),
10541058
};

0 commit comments

Comments
 (0)