diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 1797b6dcaa..2bd94de5bb 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -773,7 +773,42 @@ pub const Interpreter = struct { cwd_: ?[]const u8, ) shell.Result(*ThisInterpreter) { const export_env = brk: { - if (event_loop == .js) break :brk if (export_env_) |e| e else EnvMap.init(allocator); + if (event_loop == .js) { + var env_map = if (export_env_) |e| e else env_blk: { + // Initialize with current process environment for Bun.$ when no env provided + var env = EnvMap.init(allocator); + + // Copy current process environment + for (std.os.environ) |env_ptr| { + const env_str = bun.span(env_ptr); + if (bun.strings.indexOfChar(env_str, '=')) |eq_index| { + const key = env_str[0..eq_index]; + const value = env_str[eq_index + 1..]; + if (key.len > 0) { + const key_envstr = EnvStr.initSlice(key); + const value_envstr = EnvStr.initSlice(value); + env.insert(key_envstr, value_envstr); + } + } + } + + break :env_blk env; + }; + + // Always set SHELL environment variable to Bun executable path for Bun.$ + if (bun.selfExePath()) |self_exe_path| { + const shell_key = EnvStr.initSlice("SHELL"); + const shell_value = EnvStr.initSlice(self_exe_path); + env_map.insert(shell_key, shell_value); + } else |_| { + // If we can't get the executable path, fall back to a default + const shell_key = EnvStr.initSlice("SHELL"); + const shell_value = EnvStr.initSlice("bun"); + env_map.insert(shell_key, shell_value); + } + + break :brk env_map; + } var env_loader: *bun.DotEnv.Loader = env_loader: { if (event_loop == .js) { @@ -794,6 +829,18 @@ pub const Interpreter = struct { export_env.insert(key, value); } + // Set SHELL environment variable to Bun executable path + if (bun.selfExePath()) |self_exe_path| { + const shell_key = EnvStr.initSlice("SHELL"); + const shell_value = EnvStr.initSlice(self_exe_path); + export_env.insert(shell_key, shell_value); + } else |_| { + // If we can't get the executable path, fall back to a default + const shell_key = EnvStr.initSlice("SHELL"); + const shell_value = EnvStr.initSlice("bun"); + export_env.insert(shell_key, shell_value); + } + break :brk export_env; }; diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index e4f04cdd5c..d932c61042 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -444,6 +444,39 @@ describe("bunshell", () => { }); }); + describe("SHELL environment variable", () => { + test("should be set to Bun executable path", async () => { + const { stdout } = await $`echo $SHELL`; + const shellPath = stdout.toString().trim(); + + // Should contain "bun" in the path + expect(shellPath).toContain("bun"); + + // Should be an absolute path + expect(shellPath).toMatch(/^\/|^[A-Z]:\\/); + }); + + test("should be set in shell scripts", async () => { + const tempdir = tmpdirSync(); + const scriptPath = join(tempdir, "test_shell.sh"); + + // Create a shell script that prints SHELL + await Bun.write(scriptPath, 'echo "SHELL: $SHELL"'); + + const result = await $`bun ${scriptPath}`.text(); + + expect(result).toContain("SHELL:"); + expect(result).toContain("bun"); + }); + + TestBuilder.command`echo $SHELL` + .stdout(stdout => { + expect(stdout.trim()).toContain("bun"); + expect(stdout.trim()).toMatch(/^\/|^[A-Z]:\\/); + }) + .runAsTest("SHELL via TestBuilder"); + }); + // Ported from GNU bash "quote.tests" // https://github.com/bminor/bash/blob/f3b6bd19457e260b65d11f2712ec3da56cef463f/tests/quote.tests#L1 // Some backtick tests are skipped, because of insane behavior: