Repository: reubeno/brush
Pull request #197 (2024-10-10 21:48:48)

chore: where possible replace `async-trait` with native async trait support in 1.75+
As of Rust 1.75, the `async_fn_in_trait` feature has been merged (see: https://github.com/rust-lang/rust/pull/115822), meaning the async-trait crate is now only required for dynamic dispatch (dyn Trait). Since brush doesn’t use `dyn Command`, I decided to try removing the dependency. ~~The modules `arithmetic.rs` and `interp.rs` are most affected, as they use async recursion, which requires futures to be boxed. I’ve wrapped all recursive functions in `async move { .. }.boxed()`.~~

github-actions[bot]: # Performance Benchmark Report
| Benchmark name                         | Baseline (μs) |  Test/PR (μs) |    Delta (μs) | Delta %         |
| -------------------------------------- | ------------- | ------------- | ------------- | --------------- |
| `expand_one_string                   ` | `    3.35 μs` | `    3.34 μs` | `   -0.01 μs` | `⚪  Unchanged` |
| `instantiate_shell                   ` | `   60.55 μs` | `   60.76 μs` | `    0.20 μs` | `⚪  Unchanged` |
| `instantiate_shell_with_init_scripts ` | `28947.51 μs` | `28784.94 μs` | ` -162.57 μs` | `⚪  Unchanged` |
| `parse_bash_completion               ` | ` 2722.28 μs` | ` 2709.82 μs` | `  -12.46 μs` | `⚪  Unchanged` |
| `parse_sample_script                 ` | `    4.08 μs` | `    3.95 μs` | `   -0.14 μs` | `🟢 -3.31%    ` |
| `run_echo_builtin_command            ` | `   90.67 μs` | `   89.26 μs` | `   -1.41 μs` | `⚪  Unchanged` |
| `run_one_builtin_command             ` | `  109.86 μs` | `  108.34 μs` | `   -1.52 μs` | `⚪  Unchanged` |
| `run_one_external_command            ` | ` 2044.98 μs` | ` 1917.89 μs` | ` -127.09 μs` | `🟢 -6.21%    ` |
| `run_one_external_command_directly   ` | `  998.25 μs` | ` 1009.81 μs` | `   11.56 μs` | `⚪  Unchanged` |

# Code Coverage Report: Only Changed Files listed

| Package | Base Coverage | New Coverage | Difference |
| ------- | ------------- | ------------ | ---------- |
| brush-core/src/arithmetic.rs | 🟢 97.42% | 🟢 97.46% | 🟢 0.04% |
| brush-core/src/builtins/alias.rs | 🟠 66.67% | 🟠 70.37% | 🟢 3.7% |
| brush-core/src/builtins/break_.rs | 🟢 77.78% | 🟢 84.62% | 🟢 6.84% |
| brush-core/src/builtins/builtin_.rs | 🟢 94.12% | 🟢 95% | 🟢 0.88% |
| brush-core/src/builtins/cd.rs | 🟠 75% | 🟢 78.57% | 🟢 3.57% |
| brush-core/src/builtins/command.rs | 🟢 91.67% | 🟢 91.95% | 🟢 0.28% |
| brush-core/src/builtins/complete.rs | 🔴 20.78% | 🔴 21.07% | 🟢 0.29% |
| brush-core/src/builtins/continue_.rs | 🟢 81.82% | 🟢 86.67% | 🟢 4.85% |
| brush-core/src/builtins/declare.rs | 🟢 81.64% | 🟢 81.79% | 🟢 0.15% |
| brush-core/src/builtins/dirs.rs | 🟢 84.38% | 🟢 85.71% | 🟢 1.33% |
| brush-core/src/builtins/dot.rs | 🟢 90% | 🟢 91.67% | 🟢 1.67% |
| brush-core/src/builtins/echo.rs | 🟢 86.05% | 🟢 86.96% | 🟢 0.91% |
| brush-core/src/builtins/enable.rs | 🟠 71.74% | 🟠 73.47% | 🟢 1.73% |
| brush-core/src/builtins/eval.rs | 🟢 90.91% | 🟢 92.86% | 🟢 1.95% |
| brush-core/src/builtins/exec.rs | 🟢 85.19% | 🟢 86.67% | 🟢 1.48% |
| brush-core/src/builtins/export.rs | 🟠 72.55% | 🟠 74.07% | 🟢 1.52% |
| brush-core/src/builtins/getopts.rs | 🟢 90.79% | 🟢 91.14% | 🟢 0.35% |
| brush-core/src/builtins/help.rs | 🟢 89.02% | 🟢 89.41% | 🟢 0.39% |
| brush-core/src/builtins/jobs.rs | 🔴 25.64% | 🔴 30.95% | 🟢 5.31% |
| brush-core/src/builtins/let_.rs | 🟢 88.89% | 🟢 90.48% | 🟢 1.59% |
| brush-core/src/builtins/popd.rs | 🟢 91.67% | 🟢 93.33% | 🟢 1.66% |
| brush-core/src/builtins/printf.rs | 🟢 91.89% | 🟢 92.5% | 🟢 0.61% |
| brush-core/src/builtins/pushd.rs | 🟢 88.89% | 🟢 90.48% | 🟢 1.59% |
| brush-core/src/builtins/pwd.rs | 🟠 69.23% | 🟢 80.95% | 🟢 11.72% |
| brush-core/src/builtins/read.rs | 🟠 64.33% | 🟠 65% | 🟢 0.67% |
| brush-core/src/builtins/return_.rs | 🟠 75% | 🟢 81.82% | 🟢 6.82% |
| brush-core/src/builtins/set.rs | 🟠 68.47% | 🟠 69.3% | 🟢 0.83% |
| brush-core/src/builtins/shift.rs | 🟢 94.44% | 🟢 95.24% | 🟢 0.8% |
| brush-core/src/builtins/shopt.rs | 🟠 69.05% | 🟠 70.11% | 🟢 1.06% |
| brush-core/src/builtins/test.rs | 🟢 85% | 🟢 86.96% | 🟢 1.96% |
| brush-core/src/builtins/trap.rs | 🟠 67.9% | 🟠 69.05% | 🟢 1.15% |
| brush-core/src/builtins/type_.rs | 🟢 79.01% | 🟢 79.76% | 🟢 0.75% |
| brush-core/src/builtins/unalias.rs | 🟢 85% | 🟢 86.96% | 🟢 1.96% |
| brush-core/src/builtins/unset.rs | 🟢 80% | 🟢 82.61% | 🟢 2.61% |
| brush-core/src/builtins/wait.rs | 🟠 65% | 🟠 69.57% | 🟢 4.57% |
| brush-core/src/interp.rs | 🟢 90.18% | 🟢 91.06% | 🟢 0.88% |
| brush-core/src/shell.rs | 🟢 76.13% | 🟢 76.28% | 🟢 0.15% |
| brush-interactive/src/interactive_shell.rs | 🟠 68.25% | 🟠 71.43% | 🟢 3.18% |
| brush-shell/src/brushctl.rs | 🔴 17.14% | 🔴 15.38% | 🔴 -1.76% |
| brush-shell/src/main.rs | 🟢 89.61% | 🟢 90.26% | 🟢 0.65% |
| brush-shell/src/shell_factory.rs | 🔴 23.08% | 🔴 34.78% | 🟢 11.7% |
| **Overall Coverage** | **🟢 71.93%** | **🟢 72.24%** | **🟢 0.31%** |

_Minimum allowed coverage is_ `70%`_, this run produced_ `72.24%`
<!-- Sticky Pull Request Comment -->
39555: That's what `async_trait` or `async_recursion` do, but `async_recursion` does not work with an empty trait function body. I will revert the recursive functions since `async_trait` is really nice, and I forgot `Pin<..>` anyway. At least other traits without recursion would benefit from the native async though.
reubeno: That sounds like a better direction. Let's remove unnecessary `#[async_trait::async_trait]` attributes where it should now "just work natively". For other places where we'd have to make the code less readable, let's hold off for now.
39555: @reubeno done

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 9c914c9..8ff61bc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -572,9 +572,9 @@ checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
 
 [[package]]
 name = "clap_mangen"
-version = "0.2.23"
+version = "0.2.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb"
+checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf"
 dependencies = [
  "clap",
  "roff",
@@ -1230,9 +1230,9 @@ dependencies = [
 
 [[package]]
 name = "human-panic"
-version = "2.0.1"
+version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c5a08ed290eac04006e21e63d32e90086b6182c7cd0452d10f4264def1fec9a"
+checksum = "80b84a66a325082740043a6c28bbea400c129eac0d3a27673a1de971e44bf1f7"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1671,9 +1671,9 @@ dependencies = [
 
 [[package]]
 name = "pathdiff"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
 
 [[package]]
 name = "peg"
@@ -1823,24 +1823,23 @@ dependencies = [
 
 [[package]]
 name = "procfs"
-version = "0.16.0"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4"
+checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f"
 dependencies = [
  "bitflags 2.6.0",
  "chrono",
  "flate2",
  "hex",
- "lazy_static",
  "procfs-core",
  "rustix",
 ]
 
 [[package]]
 name = "procfs-core"
-version = "0.16.0"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29"
+checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec"
 dependencies = [
  "bitflags 2.6.0",
  "chrono",
diff --git a/brush-core/Cargo.toml b/brush-core/Cargo.toml
index 7531a41..721eef3 100644
--- a/brush-core/Cargo.toml
+++ b/brush-core/Cargo.toml
@@ -63,7 +63,7 @@ nix = { version = "0.29.0", features = [
 uzers = "0.12.1"
 
 [target.'cfg(target_os = "linux")'.dependencies]
-procfs = "0.16.0"
+procfs = "0.17.0"
 
 [dev-dependencies]
 anyhow = "1.0.89"
diff --git a/brush-core/src/builtins.rs b/brush-core/src/builtins.rs
index 2d19bb0..7ba0912 100644
--- a/brush-core/src/builtins.rs
+++ b/brush-core/src/builtins.rs
@@ -170,7 +170,7 @@ pub type CommandExecuteFunc = fn(
 pub type CommandContentFunc = fn(&str, ContentType) -> Result<String, error::Error>;
 
 /// Trait implemented by built-in shell commands.
-#[async_trait::async_trait]
+
 pub trait Command: Parser {
     /// Instantiates the built-in command with the given arguments.
     ///
@@ -208,10 +208,11 @@ pub trait Command: Parser {
     /// # Arguments
     ///
     /// * `context` - The context in which the command is being executed.
-    async fn execute(
+    // NOTE: we use desugared async here because we need a Send marker
+    fn execute(
         &self,
         context: commands::ExecutionContext<'_>,
-    ) -> Result<ExitCode, error::Error>;
+    ) -> impl std::future::Future<Output = Result<ExitCode, error::Error>> + std::marker::Send;
 
     /// Returns the textual help content associated with the command.
     ///
@@ -236,7 +237,7 @@ pub trait Command: Parser {
 
 /// Trait implemented by built-in shell commands that take specially handled declarations
 /// as arguments.
-#[async_trait::async_trait]
+
 pub trait DeclarationCommand: Command {
     /// Stores the declarations within the command instance.
     ///
@@ -395,8 +396,7 @@ fn brush_help_styles() -> clap::builder::Styles {
 /// # Returns
 ///
 /// * a parsed struct T from [`clap::Parser::parse_from`]
-/// * the remain iterator `args` with `--` and the rest arguments if they present
-///   othervise None
+/// * the remain iterator `args` with `--` and the rest arguments if they present othervise None
 ///
 /// # Examples
 /// ```
@@ -420,8 +420,8 @@ where
     S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
 {
     let mut args = args.into_iter();
-    // the best way to save `--` is to get it out with a side effect while `clap` iterates over the args
-    // this way we can be 100% sure that we have '--' and the remaining args
+    // the best way to save `--` is to get it out with a side effect while `clap` iterates over the
+    // args this way we can be 100% sure that we have '--' and the remaining args
     // and we will iterate only once
     let mut hyphen = None;
     let args_before_hyphen = args.by_ref().take_while(|a| {
@@ -437,7 +437,8 @@ where
 }
 
 /// Similar to [`parse_known`] but with [`clap::Parser::try_parse_from`]
-/// This function is used to parse arguments in builtins such as [`crate::builtins::echo::EchoCommand`]
+/// This function is used to parse arguments in builtins such as
+/// [`crate::builtins::echo::EchoCommand`]
 pub fn try_parse_known<T: Parser>(
     args: impl IntoIterator<Item = String>,
 ) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
diff --git a/brush-core/src/builtins/alias.rs b/brush-core/src/builtins/alias.rs
index 375c062..7b9f7c2 100644
--- a/brush-core/src/builtins/alias.rs
+++ b/brush-core/src/builtins/alias.rs
@@ -15,7 +15,6 @@ pub(crate) struct AliasCommand {
     aliases: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for AliasCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/bg.rs b/brush-core/src/builtins/bg.rs
index ed14d5f..7faca92 100644
--- a/brush-core/src/builtins/bg.rs
+++ b/brush-core/src/builtins/bg.rs
@@ -10,7 +10,6 @@ pub(crate) struct BgCommand {
     job_specs: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for BgCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/break_.rs b/brush-core/src/builtins/break_.rs
index 15b2c24..246c15b 100644
--- a/brush-core/src/builtins/break_.rs
+++ b/brush-core/src/builtins/break_.rs
@@ -10,7 +10,6 @@ pub(crate) struct BreakCommand {
     which_loop: i8,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for BreakCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/brushinfo.rs b/brush-core/src/builtins/brushinfo.rs
index 3da0720..9db907c 100644
--- a/brush-core/src/builtins/brushinfo.rs
+++ b/brush-core/src/builtins/brushinfo.rs
@@ -53,7 +53,6 @@ enum CompleteCommand {
     },
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for BrushInfoCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/builtin_.rs b/brush-core/src/builtins/builtin_.rs
index 8075405..a6fbd42 100644
--- a/brush-core/src/builtins/builtin_.rs
+++ b/brush-core/src/builtins/builtin_.rs
@@ -14,7 +14,6 @@ pub(crate) struct BuiltinCommand {
     args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for BuiltinCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/cd.rs b/brush-core/src/builtins/cd.rs
index 860c3c9..319b4c6 100644
--- a/brush-core/src/builtins/cd.rs
+++ b/brush-core/src/builtins/cd.rs
@@ -5,7 +5,7 @@ use clap::Parser;
 
 use crate::{builtins, commands};
 
-/// Change the current working directory.
+/// Change the current shell working directory.
 #[derive(Parser)]
 pub(crate) struct CdCommand {
     /// Force following symlinks.
@@ -16,7 +16,7 @@ pub(crate) struct CdCommand {
     #[arg(short = 'P')]
     use_physical_dir: bool,
 
-    /// Exit if current working dir resolution fails.
+    /// Exit with non zero exit status if current working directory resolution fails.
     #[arg(short = 'e')]
     exit_on_failed_cwd_resolution: bool,
 
@@ -25,10 +25,11 @@ pub(crate) struct CdCommand {
     #[arg(short = '@')]
     file_with_xattr_as_dir: bool,
 
+    /// By default it is the value of the HOME shell variable. If `TARGET_DIR` is "-", it is
+    /// converted to $OLDPWD.
     target_dir: Option<PathBuf>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for CdCommand {
     async fn execute(
         &self,
@@ -43,8 +44,22 @@ impl builtins::Command for CdCommand {
             return crate::error::unimp("options to cd");
         }
 
+        let mut should_print = false;
         let target_dir = if let Some(target_dir) = &self.target_dir {
-            target_dir.clone()
+            // `cd -', equivalent to `cd $OLDPWD'
+            if target_dir.as_os_str() == "-" {
+                should_print = true;
+                if let Some(oldpwd) = context.shell.env.get_str("OLDPWD") {
+                    PathBuf::from(oldpwd.to_string())
+                } else {
+                    writeln!(context.stderr(), "OLDPWD not set")?;
+                    return Ok(builtins::ExitCode::Custom(1));
+                }
+            } else {
+                // TODO: remove clone, and use temporary lifetime extension after rust 1.75
+                target_dir.clone()
+            }
+        // `cd' without arguments is equivalent to `cd $HOME'
         } else {
             if let Some(home_var) = context.shell.env.get_str("HOME") {
                 PathBuf::from(home_var.to_string())
@@ -54,12 +69,18 @@ impl builtins::Command for CdCommand {
             }
         };
 
-        match context.shell.set_working_dir(&target_dir) {
-            Ok(()) => {}
-            Err(e) => {
-                writeln!(context.stderr(), "cd: {e}")?;
-                return Ok(builtins::ExitCode::Custom(1));
-            }
+        if let Err(e) = context.shell.set_working_dir(&target_dir) {
+            writeln!(context.stderr(), "cd: {e}")?;
+            return Ok(builtins::ExitCode::Custom(1));
+        }
+
+        // Bash compatibility
+        // https://www.gnu.org/software/bash/manual/bash.html#index-cd
+        // If a non-empty directory name from CDPATH is used, or if '-' is the first argument, and
+        // the directory change is successful, the absolute pathname of the new working
+        // directory is written to the standard output.
+        if should_print {
+            writeln!(context.stdout(), "{}", target_dir.display())?;
         }
 
         Ok(builtins::ExitCode::Success)
diff --git a/brush-core/src/builtins/colon.rs b/brush-core/src/builtins/colon.rs
index 50640ee..371d1ed 100644
--- a/brush-core/src/builtins/colon.rs
+++ b/brush-core/src/builtins/colon.rs
@@ -10,7 +10,6 @@ pub(crate) struct ColonCommand {
     args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for ColonCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/command.rs b/brush-core/src/builtins/command.rs
index bc0c2ea..eb6ed8b 100644
--- a/brush-core/src/builtins/command.rs
+++ b/brush-core/src/builtins/command.rs
@@ -26,7 +26,6 @@ pub(crate) struct CommandCommand {
     args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for CommandCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/complete.rs b/brush-core/src/builtins/complete.rs
index 0f70bfd..dcbf23c 100644
--- a/brush-core/src/builtins/complete.rs
+++ b/brush-core/src/builtins/complete.rs
@@ -210,7 +210,6 @@ pub(crate) struct CompleteCommand {
     names: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for CompleteCommand {
     async fn execute(
         &self,
@@ -435,7 +434,6 @@ pub(crate) struct CompGenCommand {
     word: Option<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for CompGenCommand {
     async fn execute(
         &self,
@@ -514,7 +512,6 @@ pub(crate) struct CompOptCommand {
     names: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for CompOptCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/continue_.rs b/brush-core/src/builtins/continue_.rs
index 117f5ff..7963118 100644
--- a/brush-core/src/builtins/continue_.rs
+++ b/brush-core/src/builtins/continue_.rs
@@ -10,7 +10,6 @@ pub(crate) struct ContinueCommand {
     which_loop: i8,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for ContinueCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/declare.rs b/brush-core/src/builtins/declare.rs
index 407af94..1515338 100644
--- a/brush-core/src/builtins/declare.rs
+++ b/brush-core/src/builtins/declare.rs
@@ -110,7 +110,6 @@ impl builtins::DeclarationCommand for DeclareCommand {
 }
 
 #[allow(clippy::too_many_lines)]
-#[async_trait::async_trait]
 impl builtins::Command for DeclareCommand {
     fn takes_plus_options() -> bool {
         true
diff --git a/brush-core/src/builtins/dirs.rs b/brush-core/src/builtins/dirs.rs
index 4ab5dd8..9024f72 100644
--- a/brush-core/src/builtins/dirs.rs
+++ b/brush-core/src/builtins/dirs.rs
@@ -25,7 +25,6 @@ pub(crate) struct DirsCommand {
     // TODO: implement +N and -N
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for DirsCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/dot.rs b/brush-core/src/builtins/dot.rs
index 1c012e2..34b0dac 100644
--- a/brush-core/src/builtins/dot.rs
+++ b/brush-core/src/builtins/dot.rs
@@ -15,7 +15,6 @@ pub(crate) struct DotCommand {
     pub script_args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for DotCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/echo.rs b/brush-core/src/builtins/echo.rs
index ffa07de..d3f08d4 100644
--- a/brush-core/src/builtins/echo.rs
+++ b/brush-core/src/builtins/echo.rs
@@ -24,10 +24,9 @@ pub(crate) struct EchoCommand {
     args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for EchoCommand {
-    /// Override the default [builtins::Command::new] function to handle clap's limitation related to `--`.
-    /// See [crate::builtins::parse_known] for more information
+    /// Override the default [`builtins::Command::new`] function to handle clap's limitation related
+    /// to `--`. See [`builtins::parse_known`] for more information
     /// TODO: we can safely remove this after the issue is resolved
     fn new<I>(args: I) -> Result<Self, clap::Error>
     where
@@ -75,6 +74,6 @@ impl builtins::Command for EchoCommand {
         write!(context.stdout(), "{s}")?;
         context.stdout().flush()?;
 
-        return Ok(builtins::ExitCode::Success);
+        Ok(builtins::ExitCode::Success)
     }
 }
diff --git a/brush-core/src/builtins/enable.rs b/brush-core/src/builtins/enable.rs
index aa1f590..6bc0da6 100644
--- a/brush-core/src/builtins/enable.rs
+++ b/brush-core/src/builtins/enable.rs
@@ -37,7 +37,6 @@ pub(crate) struct EnableCommand {
     names: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for EnableCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/eval.rs b/brush-core/src/builtins/eval.rs
index 85887b5..c970a32 100644
--- a/brush-core/src/builtins/eval.rs
+++ b/brush-core/src/builtins/eval.rs
@@ -9,7 +9,6 @@ pub(crate) struct EvalCommand {
     pub args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for EvalCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/exec.rs b/brush-core/src/builtins/exec.rs
index 6ab3443..0137e99 100644
--- a/brush-core/src/builtins/exec.rs
+++ b/brush-core/src/builtins/exec.rs
@@ -23,7 +23,6 @@ pub(crate) struct ExecCommand {
     args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for ExecCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/exit.rs b/brush-core/src/builtins/exit.rs
index 5ce7d7e..9c5b9d0 100644
--- a/brush-core/src/builtins/exit.rs
+++ b/brush-core/src/builtins/exit.rs
@@ -9,7 +9,6 @@ pub(crate) struct ExitCommand {
     code: Option<i32>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for ExitCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/export.rs b/brush-core/src/builtins/export.rs
index 28c48b9..19541f4 100644
--- a/brush-core/src/builtins/export.rs
+++ b/brush-core/src/builtins/export.rs
@@ -37,7 +37,6 @@ impl builtins::DeclarationCommand for ExportCommand {
     }
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for ExportCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/false_.rs b/brush-core/src/builtins/false_.rs
index d837939..a2be7aa 100644
--- a/brush-core/src/builtins/false_.rs
+++ b/brush-core/src/builtins/false_.rs
@@ -6,7 +6,6 @@ use crate::{builtins, commands};
 #[derive(Parser)]
 pub(crate) struct FalseCommand {}
 
-#[async_trait::async_trait]
 impl builtins::Command for FalseCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/fg.rs b/brush-core/src/builtins/fg.rs
index e553aad..231d38c 100644
--- a/brush-core/src/builtins/fg.rs
+++ b/brush-core/src/builtins/fg.rs
@@ -10,7 +10,6 @@ pub(crate) struct FgCommand {
     job_spec: Option<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for FgCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/getopts.rs b/brush-core/src/builtins/getopts.rs
index 895b926..3d97160 100644
--- a/brush-core/src/builtins/getopts.rs
+++ b/brush-core/src/builtins/getopts.rs
@@ -18,7 +18,6 @@ pub(crate) struct GetOptsCommand {
     args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for GetOptsCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/help.rs b/brush-core/src/builtins/help.rs
index 014c7d0..32d0fde 100644
--- a/brush-core/src/builtins/help.rs
+++ b/brush-core/src/builtins/help.rs
@@ -22,7 +22,6 @@ pub(crate) struct HelpCommand {
     topic_patterns: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for HelpCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/jobs.rs b/brush-core/src/builtins/jobs.rs
index e26b707..b0ef597 100644
--- a/brush-core/src/builtins/jobs.rs
+++ b/brush-core/src/builtins/jobs.rs
@@ -31,7 +31,6 @@ pub(crate) struct JobsCommand {
     job_specs: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for JobsCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/kill.rs b/brush-core/src/builtins/kill.rs
index 342e3b5..f94b978 100644
--- a/brush-core/src/builtins/kill.rs
+++ b/brush-core/src/builtins/kill.rs
@@ -16,7 +16,6 @@ pub(crate) struct KillCommand {
 
     //
     // TODO: implement -sigspec syntax
-    //
     /// List known signal names.
     #[arg(short = 'l', short_alias = 'L')]
     list_signals: bool,
@@ -25,7 +24,6 @@ pub(crate) struct KillCommand {
     args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for KillCommand {
     async fn execute(
         &self,
@@ -39,7 +37,7 @@ impl builtins::Command for KillCommand {
         }
 
         if self.list_signals {
-            return error::unimp("kill -l");
+            error::unimp("kill -l")
         } else {
             if self.args.len() != 1 {
                 writeln!(context.stderr(), "{}: invalid usage", context.command_name)?;
diff --git a/brush-core/src/builtins/let_.rs b/brush-core/src/builtins/let_.rs
index 774c662..9f2cb4a 100644
--- a/brush-core/src/builtins/let_.rs
+++ b/brush-core/src/builtins/let_.rs
@@ -11,7 +11,6 @@ pub(crate) struct LetCommand {
     exprs: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for LetCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/popd.rs b/brush-core/src/builtins/popd.rs
index ed6409d..d525feb 100644
--- a/brush-core/src/builtins/popd.rs
+++ b/brush-core/src/builtins/popd.rs
@@ -11,10 +11,8 @@ pub(crate) struct PopdCommand {
     no_directory_change: bool,
     //
     // TODO: implement +N and -N
-    //
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for PopdCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/printf.rs b/brush-core/src/builtins/printf.rs
index fcdc712..248d24a 100644
--- a/brush-core/src/builtins/printf.rs
+++ b/brush-core/src/builtins/printf.rs
@@ -16,7 +16,6 @@ pub(crate) struct PrintfCommand {
     format_and_args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for PrintfCommand {
     async fn execute(
         &self,
@@ -31,7 +30,7 @@ impl builtins::Command for PrintfCommand {
             context.stdout().flush()?;
         }
 
-        return Ok(builtins::ExitCode::Success);
+        Ok(builtins::ExitCode::Success)
     }
 }
 
diff --git a/brush-core/src/builtins/pushd.rs b/brush-core/src/builtins/pushd.rs
index 08240b2..595dedd 100644
--- a/brush-core/src/builtins/pushd.rs
+++ b/brush-core/src/builtins/pushd.rs
@@ -13,10 +13,8 @@ pub(crate) struct PushdCommand {
     dir: String,
     //
     // TODO: implement +N and -N
-    //
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for PushdCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/pwd.rs b/brush-core/src/builtins/pwd.rs
index 72874da..8ad06db 100644
--- a/brush-core/src/builtins/pwd.rs
+++ b/brush-core/src/builtins/pwd.rs
@@ -14,7 +14,6 @@ pub(crate) struct PwdCommand {
     allow_symlinks: bool,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for PwdCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/read.rs b/brush-core/src/builtins/read.rs
index b3c045c..ab70f84 100644
--- a/brush-core/src/builtins/read.rs
+++ b/brush-core/src/builtins/read.rs
@@ -59,7 +59,6 @@ pub(crate) struct ReadCommand {
     variable_names: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for ReadCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/return_.rs b/brush-core/src/builtins/return_.rs
index ebcf9ef..f52e3d4 100644
--- a/brush-core/src/builtins/return_.rs
+++ b/brush-core/src/builtins/return_.rs
@@ -9,7 +9,6 @@ pub(crate) struct ReturnCommand {
     code: Option<i32>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for ReturnCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/set.rs b/brush-core/src/builtins/set.rs
index 8504e63..9fa6448 100644
--- a/brush-core/src/builtins/set.rs
+++ b/brush-core/src/builtins/set.rs
@@ -130,7 +130,6 @@ pub(crate) struct SetCommand {
     positional_args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for SetCommand {
     fn takes_plus_options() -> bool {
         true
diff --git a/brush-core/src/builtins/shift.rs b/brush-core/src/builtins/shift.rs
index e81bf77..df38628 100644
--- a/brush-core/src/builtins/shift.rs
+++ b/brush-core/src/builtins/shift.rs
@@ -9,7 +9,6 @@ pub(crate) struct ShiftCommand {
     n: Option<i32>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for ShiftCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/shopt.rs b/brush-core/src/builtins/shopt.rs
index a3d4477..d1604b2 100644
--- a/brush-core/src/builtins/shopt.rs
+++ b/brush-core/src/builtins/shopt.rs
@@ -31,7 +31,6 @@ pub(crate) struct ShoptCommand {
     options: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for ShoptCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/trap.rs b/brush-core/src/builtins/trap.rs
index 69e24e9..1a9cc71 100644
--- a/brush-core/src/builtins/trap.rs
+++ b/brush-core/src/builtins/trap.rs
@@ -17,7 +17,6 @@ pub(crate) struct TrapCommand {
     args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for TrapCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/true_.rs b/brush-core/src/builtins/true_.rs
index 99ca5a0..c70c34c 100644
--- a/brush-core/src/builtins/true_.rs
+++ b/brush-core/src/builtins/true_.rs
@@ -6,7 +6,6 @@ use crate::{builtins, commands};
 #[derive(Parser)]
 pub(crate) struct TrueCommand {}
 
-#[async_trait::async_trait]
 impl builtins::Command for TrueCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/type_.rs b/brush-core/src/builtins/type_.rs
index fded6e2..8723981 100644
--- a/brush-core/src/builtins/type_.rs
+++ b/brush-core/src/builtins/type_.rs
@@ -44,7 +44,6 @@ enum ResolvedType {
     File(PathBuf),
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for TypeCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/umask.rs b/brush-core/src/builtins/umask.rs
index 8a83f3b..3b8c45d 100644
--- a/brush-core/src/builtins/umask.rs
+++ b/brush-core/src/builtins/umask.rs
@@ -20,7 +20,6 @@ pub(crate) struct UmaskCommand {
     mode: Option<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for UmaskCommand {
     async fn execute(
         &self,
@@ -28,7 +27,7 @@ impl builtins::Command for UmaskCommand {
     ) -> Result<crate::builtins::ExitCode, crate::error::Error> {
         if let Some(mode) = &self.mode {
             if mode.starts_with(|c: char| c.is_digit(8)) {
-                let parsed = u32::from_str_radix(mode.as_str(), 8)?;
+                let parsed = nix::sys::stat::mode_t::from_str_radix(mode.as_str(), 8)?;
                 set_umask(parsed)?;
             } else {
                 return crate::error::unimp("umask setting mode from symbolic value");
@@ -64,17 +63,18 @@ cfg_if! {
             status.umask.ok_or_else(|| error::Error::InvalidUmask)
         }
     } else {
+        #[allow(clippy::unnecessary_wraps)]
         fn get_umask() -> Result<u32, error::Error> {
             let u = nix::sys::stat::umask(Mode::empty());
             nix::sys::stat::umask(u);
-            Ok(u.bits() as u32)
+            Ok(u32::from(u.bits()))
         }
     }
 }
 
-fn set_umask(value: u32) -> Result<(), error::Error> {
-    let mode =
-        nix::sys::stat::Mode::from_bits(value as _).ok_or_else(|| error::Error::InvalidUmask)?;
+fn set_umask(value: nix::sys::stat::mode_t) -> Result<(), error::Error> {
+    // value of mode_t can be platform dependent
+    let mode = nix::sys::stat::Mode::from_bits(value).ok_or_else(|| error::Error::InvalidUmask)?;
     nix::sys::stat::umask(mode);
     Ok(())
 }
diff --git a/brush-core/src/builtins/unalias.rs b/brush-core/src/builtins/unalias.rs
index 6c5c5c3..50992a9 100644
--- a/brush-core/src/builtins/unalias.rs
+++ b/brush-core/src/builtins/unalias.rs
@@ -14,7 +14,6 @@ pub(crate) struct UnaliasCommand {
     aliases: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for UnaliasCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/unimp.rs b/brush-core/src/builtins/unimp.rs
index 67afa64..db60c69 100644
--- a/brush-core/src/builtins/unimp.rs
+++ b/brush-core/src/builtins/unimp.rs
@@ -13,7 +13,6 @@ pub(crate) struct UnimplementedCommand {
     declarations: Vec<commands::CommandArg>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for UnimplementedCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/unset.rs b/brush-core/src/builtins/unset.rs
index b212091..94ac005 100644
--- a/brush-core/src/builtins/unset.rs
+++ b/brush-core/src/builtins/unset.rs
@@ -34,7 +34,6 @@ impl UnsetNameInterpretation {
     }
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for UnsetCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/builtins/wait.rs b/brush-core/src/builtins/wait.rs
index 049164b..c8dbf0b 100644
--- a/brush-core/src/builtins/wait.rs
+++ b/brush-core/src/builtins/wait.rs
@@ -22,7 +22,6 @@ pub(crate) struct WaitCommand {
     job_specs: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for WaitCommand {
     async fn execute(
         &self,
diff --git a/brush-core/src/interp.rs b/brush-core/src/interp.rs
index 1b986b0..bacdf0e 100644
--- a/brush-core/src/interp.rs
+++ b/brush-core/src/interp.rs
@@ -1048,19 +1048,15 @@ async fn expand_assignment_value(
         ast::AssignmentValue::Array(arr) => {
             let mut expanded_values = vec![];
             for (key, value) in arr {
-                match key {
-                    Some(k) => {
-                        let expanded_key = expansion::basic_expand_word(shell, k).await?.into();
-                        let expanded_value =
-                            expansion::basic_expand_word(shell, value).await?.into();
-                        expanded_values.push((Some(expanded_key), expanded_value));
-                    }
-                    None => {
-                        let split_expanded_value =
-                            expansion::full_expand_and_split_word(shell, value).await?;
-                        for expanded_value in split_expanded_value {
-                            expanded_values.push((None, expanded_value.into()));
-                        }
+                if let Some(k) = key {
+                    let expanded_key = expansion::basic_expand_word(shell, k).await?.into();
+                    let expanded_value = expansion::basic_expand_word(shell, value).await?.into();
+                    expanded_values.push((Some(expanded_key), expanded_value));
+                } else {
+                    let split_expanded_value =
+                        expansion::full_expand_and_split_word(shell, value).await?;
+                    for expanded_value in split_expanded_value {
+                        expanded_values.push((None, expanded_value.into()));
                     }
                 }
             }
diff --git a/brush-core/src/shell.rs b/brush-core/src/shell.rs
index 3190bed..72ab6ca 100644
--- a/brush-core/src/shell.rs
+++ b/brush-core/src/shell.rs
@@ -969,8 +969,6 @@ impl Shell {
 
         let pwd = cleaned_path.to_string_lossy().to_string();
 
-        // TODO: handle updating PWD
-        self.working_dir = cleaned_path;
         self.env.update_or_add(
             "PWD",
             variables::ShellValueLiteral::Scalar(pwd),
@@ -981,6 +979,18 @@ impl Shell {
             EnvironmentLookup::Anywhere,
             EnvironmentScope::Global,
         )?;
+        let oldpwd = std::mem::replace(&mut self.working_dir, cleaned_path);
+
+        self.env.update_or_add(
+            "OLDPWD",
+            variables::ShellValueLiteral::Scalar(oldpwd.to_string_lossy().to_string()),
+            |var| {
+                var.export();
+                Ok(())
+            },
+            EnvironmentLookup::Anywhere,
+            EnvironmentScope::Global,
+        )?;
 
         Ok(())
     }
diff --git a/brush-core/src/variables.rs b/brush-core/src/variables.rs
index ec3848f..1d5470c 100644
--- a/brush-core/src/variables.rs
+++ b/brush-core/src/variables.rs
@@ -215,8 +215,8 @@ impl ShellVariable {
 
         if append {
             match (&self.value, &value) {
-                // If we're appending an array to a declared-but-unset variable (or appending anything to a declared-but-unset array),
-                // then fill it out first.
+                // If we're appending an array to a declared-but-unset variable (or appending
+                // anything to a declared-but-unset array), then fill it out first.
                 (ShellValue::Unset(_), ShellValueLiteral::Array(_))
                 | (
                     ShellValue::Unset(
@@ -226,8 +226,8 @@ impl ShellVariable {
                 ) => {
                     self.assign(ShellValueLiteral::Array(ArrayLiteral(vec![])), false)?;
                 }
-                // If we're trying to append an array to a string, we first promote the string to be an array
-                // with the string being present at index 0.
+                // If we're trying to append an array to a string, we first promote the string to be
+                // an array with the string being present at index 0.
                 (ShellValue::String(_), ShellValueLiteral::Array(_)) => {
                     self.convert_to_indexed_array()?;
                 }
@@ -289,9 +289,9 @@ impl ShellVariable {
                     ShellValueLiteral::Scalar(s),
                 ) => self.assign_at_index(String::from("0"), s, false),
 
-                // If we're updating an indexed array value with an array, then preserve the array type.
-                // We also default to using an indexed array if we are assigning an array to a previously
-                // string-holding variable.
+                // If we're updating an indexed array value with an array, then preserve the array
+                // type. We also default to using an indexed array if we are
+                // assigning an array to a previously string-holding variable.
                 (
                     ShellValue::IndexedArray(_)
                     | ShellValue::Unset(
@@ -305,7 +305,8 @@ impl ShellVariable {
                     Ok(())
                 }
 
-                // If we're updating an associative array value with an array, then preserve the array type.
+                // If we're updating an associative array value with an array, then preserve the
+                // array type.
                 (
                     ShellValue::AssociativeArray(_)
                     | ShellValue::Unset(ShellValueUnsetType::AssociativeArray),
@@ -334,7 +335,8 @@ impl ShellVariable {
     ///
     /// * `array_index` - The index at which to assign the value.
     /// * `value` - The value to assign to the variable at the given index.
-    /// * `append` - Whether or not to append the value to the preexisting value stored at the given index.
+    /// * `append` - Whether or not to append the value to the preexisting value stored at the given
+    ///   index.
     #[allow(clippy::needless_pass_by_value)]
     pub fn assign_at_index(
         &mut self,
diff --git a/brush-interactive/src/interactive_shell.rs b/brush-interactive/src/interactive_shell.rs
index 3d8921c..914c09a 100644
--- a/brush-interactive/src/interactive_shell.rs
+++ b/brush-interactive/src/interactive_shell.rs
@@ -32,7 +32,7 @@ pub struct InteractivePrompt {
 }
 
 /// Represents a shell capable of taking commands from standard input.
-#[async_trait::async_trait]
+
 pub trait InteractiveShell {
     /// Returns an immutable reference to the inner shell object.
     fn shell(&self) -> impl AsRef<brush_core::Shell> + Send;
@@ -53,94 +53,102 @@ pub trait InteractiveShell {
     /// Runs the interactive shell loop, reading commands from standard input and writing
     /// results to standard output and standard error. Continues until the shell
     /// normally exits or until a fatal error occurs.
-    async fn run_interactively(&mut self) -> Result<(), ShellError> {
-        // TODO: Consider finding a better place for this.
-        let _ = brush_core::TerminalControl::acquire()?;
-
-        loop {
-            let result = self.run_interactively_once().await?;
-            match result {
-                InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
-                    exit_shell,
-                    return_from_function_or_script,
-                    ..
-                }) => {
-                    if exit_shell {
-                        break;
+    // NOTE: we use desugared async here because [async_fn_in_trait] "warning: use of `async fn` in
+    // public traits is discouraged as auto trait bounds cannot be specified"
+    fn run_interactively(&mut self) -> impl std::future::Future<Output = Result<(), ShellError>> {
+        async {
+            // TODO: Consider finding a better place for this.
+            let _ = brush_core::TerminalControl::acquire()?;
+
+            loop {
+                let result = self.run_interactively_once().await?;
+                match result {
+                    InteractiveExecutionResult::Executed(brush_core::ExecutionResult {
+                        exit_shell,
+                        return_from_function_or_script,
+                        ..
+                    }) => {
+                        if exit_shell {
+                            break;
+                        }
+
+                        if return_from_function_or_script {
+                            tracing::error!("return from non-function/script");
+                        }
                     }
-
-                    if return_from_function_or_script {
-                        tracing::error!("return from non-function/script");
+                    InteractiveExecutionResult::Failed(e) => {
+                        // Report the error, but continue to execute.
+                        tracing::error!("error: {:#}", e);
+                    }
+                    InteractiveExecutionResult::Eof => {
+                        break;
                     }
-                }
-                InteractiveExecutionResult::Failed(e) => {
-                    // Report the error, but continue to execute.
-                    tracing::error!("error: {:#}", e);
-                }
-                InteractiveExecutionResult::Eof => {
-                    break;
                 }
             }
-        }
 
-        if self.shell().as_ref().options.interactive {
-            writeln!(self.shell().as_ref().stderr(), "exit")?;
-        }
+            if self.shell().as_ref().options.interactive {
+                writeln!(self.shell().as_ref().stderr(), "exit")?;
+            }
 
-        if let Err(e) = self.update_history() {
-            // N.B. This seems like the sort of thing that's worth being noisy about,
-            // but bash doesn't do that -- and probably for a reason.
-            tracing::debug!("couldn't save history: {e}");
-        }
+            if let Err(e) = self.update_history() {
+                // N.B. This seems like the sort of thing that's worth being noisy about,
+                // but bash doesn't do that -- and probably for a reason.
+                tracing::debug!("couldn't save history: {e}");
+            }
 
-        Ok(())
+            Ok(())
+        }
     }
 
     /// Runs the interactive shell loop once, reading a single command from standard input.
-    async fn run_interactively_once(&mut self) -> Result<InteractiveExecutionResult, ShellError> {
-        let mut shell_mut = self.shell_mut();
-
-        // Check for any completed jobs.
-        shell_mut.as_mut().check_for_completed_jobs()?;
+    fn run_interactively_once(
+        &mut self,
+    ) -> impl std::future::Future<Output = Result<InteractiveExecutionResult, ShellError>> {
+        async {
+            let mut shell_mut = self.shell_mut();
 
-        // If there's a variable called PROMPT_COMMAND, then run it first.
-        if let Some((_, prompt_cmd)) = shell_mut.as_mut().env.get("PROMPT_COMMAND") {
-            let prompt_cmd = prompt_cmd.value().to_cow_string().to_string();
+            // Check for any completed jobs.
+            shell_mut.as_mut().check_for_completed_jobs()?;
 
-            // Save (and later restore) the last exit status.
-            let prev_last_result = shell_mut.as_mut().last_exit_status;
+            // If there's a variable called PROMPT_COMMAND, then run it first.
+            if let Some((_, prompt_cmd)) = shell_mut.as_mut().env.get("PROMPT_COMMAND") {
+                let prompt_cmd = prompt_cmd.value().to_cow_string().to_string();
 
-            let params = shell_mut.as_mut().default_exec_params();
+                // Save (and later restore) the last exit status.
+                let prev_last_result = shell_mut.as_mut().last_exit_status;
 
-            shell_mut.as_mut().run_string(prompt_cmd, &params).await?;
-            shell_mut.as_mut().last_exit_status = prev_last_result;
-        }
-
-        // Now that we've done that, compose the prompt.
-        let prompt = InteractivePrompt {
-            prompt: shell_mut.as_mut().compose_prompt().await?,
-            alt_side_prompt: shell_mut.as_mut().compose_alt_side_prompt().await?,
-            continuation_prompt: shell_mut.as_mut().continuation_prompt()?,
-        };
+                let params = shell_mut.as_mut().default_exec_params();
 
-        drop(shell_mut);
+                shell_mut.as_mut().run_string(prompt_cmd, &params).await?;
+                shell_mut.as_mut().last_exit_status = prev_last_result;
+            }
 
-        match self.read_line(prompt)? {
-            ReadResult::Input(read_result) => {
-                let mut shell_mut = self.shell_mut();
-                let params = shell_mut.as_mut().default_exec_params();
-                match shell_mut.as_mut().run_string(read_result, &params).await {
-                    Ok(result) => Ok(InteractiveExecutionResult::Executed(result)),
-                    Err(e) => Ok(InteractiveExecutionResult::Failed(e)),
+            // Now that we've done that, compose the prompt.
+            let prompt = InteractivePrompt {
+                prompt: shell_mut.as_mut().compose_prompt().await?,
+                alt_side_prompt: shell_mut.as_mut().compose_alt_side_prompt().await?,
+                continuation_prompt: shell_mut.as_mut().continuation_prompt()?,
+            };
+
+            drop(shell_mut);
+
+            match self.read_line(prompt)? {
+                ReadResult::Input(read_result) => {
+                    let mut shell_mut = self.shell_mut();
+                    let params = shell_mut.as_mut().default_exec_params();
+                    match shell_mut.as_mut().run_string(read_result, &params).await {
+                        Ok(result) => Ok(InteractiveExecutionResult::Executed(result)),
+                        Err(e) => Ok(InteractiveExecutionResult::Failed(e)),
+                    }
+                }
+                ReadResult::Eof => Ok(InteractiveExecutionResult::Eof),
+                ReadResult::Interrupted => {
+                    let mut shell_mut = self.shell_mut();
+                    shell_mut.as_mut().last_exit_status = 130;
+                    Ok(InteractiveExecutionResult::Executed(
+                        brush_core::ExecutionResult::new(130),
+                    ))
                 }
-            }
-            ReadResult::Eof => Ok(InteractiveExecutionResult::Eof),
-            ReadResult::Interrupted => {
-                let mut shell_mut = self.shell_mut();
-                shell_mut.as_mut().last_exit_status = 130;
-                Ok(InteractiveExecutionResult::Executed(
-                    brush_core::ExecutionResult::new(130),
-                ))
             }
         }
     }
diff --git a/brush-interactive/src/reedline/highlighter.rs b/brush-interactive/src/reedline/highlighter.rs
index 5f9036f..6b2f04d 100644
--- a/brush-interactive/src/reedline/highlighter.rs
+++ b/brush-interactive/src/reedline/highlighter.rs
@@ -244,7 +244,8 @@ impl<'a> StyledInputLine<'a> {
     }
 
     fn skip_ahead(&mut self, dest: usize) {
-        // Append a no-op style to make sure we cover any trailing gaps in the input line not otherwise styled.
+        // Append a no-op style to make sure we cover any trailing gaps in the input line not
+        // otherwise styled.
         self.append_style(Style::new(), dest, dest);
     }
 
diff --git a/brush-shell/Cargo.toml b/brush-shell/Cargo.toml
index 8a3568a..00c381f 100644
--- a/brush-shell/Cargo.toml
+++ b/brush-shell/Cargo.toml
@@ -45,7 +45,7 @@ git-version = "0.3.9"
 lazy_static = "1.5.0"
 tracing = "0.1.40"
 tracing-subscriber = "0.3.18"
-human-panic = "2.0.1"
+human-panic = "2.0.2"
 
 [target.'cfg(not(any(windows, unix)))'.dependencies]
 brush-interactive = { version = "^0.2.10", path = "../brush-interactive", features = [
@@ -72,7 +72,7 @@ expectrl = { git = "https://github.com/zhiburt/expectrl", rev = "a0f4f7816b9a47a
 glob = "0.3.1"
 indent = "0.1.1"
 junit-report = "0.8.3"
-pathdiff = "0.2.1"
+pathdiff = "0.2.2"
 regex = "1.11.0"
 serde = { version = "1.0.210", features = ["derive"] }
 serde_yaml = "0.9.34"
diff --git a/brush-shell/src/brushctl.rs b/brush-shell/src/brushctl.rs
index c50a2f9..07627d9 100644
--- a/brush-shell/src/brushctl.rs
+++ b/brush-shell/src/brushctl.rs
@@ -42,7 +42,6 @@ enum EventsCommand {
     },
 }
 
-#[async_trait::async_trait]
 impl brush_core::builtins::Command for BrushCtlCommand {
     async fn execute(
         &self,
diff --git a/brush-shell/src/shell_factory.rs b/brush-shell/src/shell_factory.rs
index bab0215..58d70c0 100644
--- a/brush-shell/src/shell_factory.rs
+++ b/brush-shell/src/shell_factory.rs
@@ -1,4 +1,3 @@
-#[async_trait::async_trait]
 pub(crate) trait ShellFactory {
     type ShellType: brush_interactive::InteractiveShell + Send;
 
@@ -53,7 +52,6 @@ impl AsMut<brush_core::Shell> for StubShell {
 
 pub(crate) struct RustylineShellFactory;
 
-#[async_trait::async_trait]
 impl ShellFactory for RustylineShellFactory {
     #[cfg(all(feature = "rustyline", any(windows, unix)))]
     type ShellType = brush_interactive::RustylineShell;
@@ -78,7 +76,6 @@ impl ShellFactory for RustylineShellFactory {
 
 pub(crate) struct ReedlineShellFactory;
 
-#[async_trait::async_trait]
 impl ShellFactory for ReedlineShellFactory {
     #[cfg(all(feature = "reedline", any(windows, unix)))]
     type ShellType = brush_interactive::ReedlineShell;
@@ -103,7 +100,6 @@ impl ShellFactory for ReedlineShellFactory {
 
 pub(crate) struct BasicShellFactory;
 
-#[async_trait::async_trait]
 impl ShellFactory for BasicShellFactory {
     #[cfg(feature = "basic")]
     type ShellType = brush_interactive::BasicShell;
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index 4f37a86..de84765 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -15,5 +15,5 @@ rust-version.workspace = true
 anyhow = "1.0.89"
 brush-shell = { version = "^0.2.10", path = "../brush-shell" }
 clap = { version = "4.5.17", features = ["derive"] }
-clap_mangen = "0.2.23"
+clap_mangen = "0.2.24"
 clap-markdown = "0.1.4"


Test patch:
diff --git a/brush-core/src/builtins/test.rs b/brush-core/src/builtins/test.rs
index bb151fb..532552c 100644
--- a/brush-core/src/builtins/test.rs
+++ b/brush-core/src/builtins/test.rs
@@ -11,7 +11,6 @@ pub(crate) struct TestCommand {
     args: Vec<String>,
 }
 
-#[async_trait::async_trait]
 impl builtins::Command for TestCommand {
     async fn execute(
         &self,
diff --git a/brush-shell/tests/cases/builtins/cd.yaml b/brush-shell/tests/cases/builtins/cd.yaml
index 667d4e3..67b860f 100644
--- a/brush-shell/tests/cases/builtins/cd.yaml
+++ b/brush-shell/tests/cases/builtins/cd.yaml
@@ -28,3 +28,16 @@ cases:
       cd file
       echo $?
       ls -1
+
+  - name: "cd -"
+    ignore_stderr: true
+    stdin: |
+      cd /
+      echo "pwd: $PWD"
+      cd usr
+      echo "pwd: $PWD"
+      echo "oldpwd: $OLDPWD"
+      cd -
+      echo $?
+      echo "pwd: $PWD"
+