Repository: reubeno/brush (rust)

Issue:
Unexpected "Bad file descriptor"
Here, I'd expect file descriptor 3 to dup stdout.
```
brush$ exec 3>&1
brush$ echo foo >&3
ERROR 3: Bad file descriptor
```
Interestingly, this works:
```
brush$ bash -c 'echo foo >&3' 3>&1
foo
```

Pull request description:
fix(builtins): implement command-less exec semantics with open fds
Resolves #383 

Thankfully, this was a simple fix. The `exec` builtin was missing logic to handle file descriptor inheritance in the command-less case, i.e., that the open files set up for the builtin need to be *persistently* propagated back to the running shell.

Adds a new test case that easily reproduced the reported issue, and now confirms it's fixed.

Patch:
diff --git a/brush-core/src/builtins/exec.rs b/brush-core/src/builtins/exec.rs
index 0137e99..69a3186 100644
--- a/brush-core/src/builtins/exec.rs
+++ b/brush-core/src/builtins/exec.rs
@@ -29,6 +29,10 @@ impl builtins::Command for ExecCommand {
         context: commands::ExecutionContext<'_>,
     ) -> Result<builtins::ExitCode, crate::error::Error> {
         if self.args.is_empty() {
+            // When no arguments are present, then there's nothing for us to execute -- but we need
+            // to ensure that any redirections setup for this builtin get applied to the calling
+            // shell instance.
+            context.shell.replace_open_files(context.params.open_files);
             return Ok(builtins::ExitCode::Success);
         }
 
diff --git a/brush-core/src/shell.rs b/brush-core/src/shell.rs
index 21ce252..b42aec4 100644
--- a/brush-core/src/shell.rs
+++ b/brush-core/src/shell.rs
@@ -1389,6 +1389,15 @@ impl Shell {
         Ok(std::fs::File::open(path_to_open)?.into())
     }
 
+    /// Replaces the shell's file descriptor table with the given one.
+    ///
+    /// # Arguments
+    ///
+    /// * `open_files` - The new file descriptor table to use.
+    pub(crate) fn replace_open_files(&mut self, open_files: openfiles::OpenFiles) {
+        self.open_files = open_files;
+    }
+
     /// Sets the shell's current working directory to the given path.
     ///
     /// # Arguments


Test patch:
diff --git a/brush-shell/tests/cases/builtins/exec.yaml b/brush-shell/tests/cases/builtins/exec.yaml
index 24df86b..1716207 100644
--- a/brush-shell/tests/cases/builtins/exec.yaml
+++ b/brush-shell/tests/cases/builtins/exec.yaml
@@ -11,6 +11,11 @@ cases:
       exec
       [[ "${pid}" == "$$" ]] || echo "PID changed"
 
+  - name: "Exec with no arguments and redirection"
+    stdin: |
+      exec 3>&1
+      echo "Hello, fd 3!" >&3
+
   - name: "Exec a command"
     stdin: |
       exec $0 -c 'echo "In child shell"'