Repository: reubeno/brush (Rust)

Issue:
Proposal: Provide a brush-specific variable
## Summary:

Add `$BRUSH_VERSION` or another global shell variable to indicate that the shell is brush.

## Motivation:

I want this feature for the gradual adoption of brush, as a workaround for compatibility issues such as #380. Until these compatibility issues are resolved, loading the same `.bashrc` file generates warnings or errors.
We can avoid this behavior by putting a special configuration file for brush; however, managing shared and brush-specific configurations is often inconvenient.
On the other hand, ignoring the whole configuration with `--norc` or `--noprofile` is also not desirable for a daily driver.
So, initially, I added a condition to skip unsupported features with `! command -v brushinfo`. However, this approach often significantly slows down original bash startup.
I'm not sure why, but I suspect it's due to the much longer `$PATH` in my use case (NixOS and/or home-manager. Windows also injects `$PATH` in WSL2). Actually, my bash startup speed increased to 140ms from 90ms.

## Justification:

As far as I know, other famous shells have similar features.

* bash: `$BASH_VERSION`
* zsh: `$ZSH_VERSION`
* fish: `$FISH_VERSION`
* nushell: `$env.NU_VERSION`
* powershell: `$PSVersionTable`
* rusty_bash: This is an exception. Sush does not have a special version variable and [returns its own version via `$BASH_VERSION`](https://github.com/shellgei/rusty_bash/blob/1d34a3145a27eb8092763eb53b7b691a405ea3b5/src/core.rs#L124-L135).

Currently, brush seems to return `$BASH_VERSION`, but this is not its own version. I think keeping the current `$BASH_VERSION` and providing a brush-specific variable like `$BRUSH_VERSION` will be useful for gradual adoption.




Discussion:
This is a great idea, thanks for putting together the proposal and PR! As you've noted, we report a fixed `$BASH_VERSION` intended to match the version of the `bash` "language" that we're aiming to support; various scripts inspect this version and utilize it as a guard for conditional use of "new" `bash` features. 

In short, `$BASH_VERSION` can be what we use to describe the bash _interface_ version we're trying to match, and `$BRUSH_VERSION` can be our true _implementation_ version.



Patch:
diff --git a/brush-core/src/shell.rs b/brush-core/src/shell.rs
index 87fbad5a..5c584c10 100644
--- a/brush-core/src/shell.rs
+++ b/brush-core/src/shell.rs
@@ -189,6 +189,8 @@ pub struct CreateOptions {
     pub max_function_call_depth: Option<usize>,
     /// Key bindings helper for the shell to use.
     pub key_bindings: Option<KeyBindingsHelper>,
+    /// Brush implementation version.
+    pub shell_version: Option<String>,
 }
 
 /// Represents an executing script.
@@ -300,6 +302,11 @@ impl Shell {
                 self.env.set_global(k, var)?;
             }
         }
+        let shell_version = options.shell_version.clone();
+        self.env.set_global(
+            "BRUSH_VERSION",
+            ShellVariable::new(shell_version.unwrap_or_default().into()),
+        )?;
 
         // TODO(#479): implement $_
 
@@ -409,6 +416,7 @@ impl Shell {
         self.env.set_global("BASH_VERSINFO", bash_versinfo_var)?;
 
         // BASH_VERSION
+        // This is the Bash interface version. See BRUSH_VERSION for its implementation version.
         self.env.set_global(
             "BASH_VERSION",
             ShellVariable::new(
diff --git a/brush-shell/src/main.rs b/brush-shell/src/main.rs
index 315977fd..d3b370c9 100644
--- a/brush-shell/src/main.rs
+++ b/brush-shell/src/main.rs
@@ -262,6 +262,7 @@ async fn instantiate_shell(
             verbose: args.verbose,
             max_function_call_depth: None,
             key_bindings: None,
+            shell_version: Some(env!("CARGO_PKG_VERSION").to_string()),
         },
         disable_bracketed_paste: args.disable_bracketed_paste,
         disable_color: args.disable_color,


Test patch:
diff --git a/brush-shell/tests/integration_tests.rs b/brush-shell/tests/integration_tests.rs
new file mode 100644
index 00000000..f6ec2b1b
--- /dev/null
+++ b/brush-shell/tests/integration_tests.rs
@@ -0,0 +1,34 @@
+//! Integration tests for brush shell
+
+// For now, only compile this for Unix-like platforms (Linux, macOS).
+#![cfg(unix)]
+#![allow(clippy::panic_in_result_fn)]
+
+use anyhow::Context;
+
+#[test]
+fn get_version_variables() -> anyhow::Result<()> {
+    let shell_path = assert_cmd::cargo::cargo_bin("brush");
+    let brush_ver_str = get_variable(&shell_path, "BRUSH_VERSION")?;
+    let bash_ver_str = get_variable(&shell_path, "BASH_VERSION")?;
+
+    assert_eq!(brush_ver_str, env!("CARGO_PKG_VERSION"));
+    assert_ne!(
+        brush_ver_str, bash_ver_str,
+        "Should differ for scripting use-case"
+    );
+
+    Ok(())
+}
+
+fn get_variable(shell_path: &std::path::Path, var: &str) -> anyhow::Result<String> {
+    let output = std::process::Command::new(shell_path)
+        .arg("--norc")
+        .arg("--noprofile")
+        .arg("-c")
+        .arg(format!("echo -n ${{{var}}}"))
+        .output()
+        .with_context(|| format!("failed to retrieve {var}"))?
+        .stdout;
+    Ok(String::from_utf8(output)?)
+}