Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
a1be62304a fix(shell): use shell PATH for command resolution
When using the Bun shell `$` template literal, setting PATH via `.env()`
or `export` was not affecting command resolution. The `which` builtin
correctly used the shell environment PATH, but actual command execution
used the process PATH instead.

The fix checks the shell environment for PATH (cmd_local_env first,
then export_env) before falling back to the process PATH, matching
how the `which` builtin already handles this.

Closes #25885

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 11:27:54 +00:00
2 changed files with 72 additions and 1 deletions

View File

@@ -485,7 +485,16 @@ fn initSubproc(this: *Cmd) Yield {
const path_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(path_buf);
const resolved = which(path_buf, spawn_args.PATH, spawn_args.cwd, first_arg_real) orelse blk: {
// Check shell environment for PATH before falling back to process PATH.
// cmd_local_env has priority (set via .env()), then export_env.
const path_key = shell.EnvStr.initSlice("PATH");
const lookup_path = if (this.base.shell.cmd_local_env.get(path_key)) |p|
p.slice()
else if (this.base.shell.export_env.get(path_key)) |p|
p.slice()
else
spawn_args.PATH;
const resolved = which(path_buf, lookup_path, spawn_args.cwd, first_arg_real) orelse blk: {
if (bun.strings.eqlComptime(first_arg_real, "bun") or bun.strings.eqlComptime(first_arg_real, "bun-debug")) blk2: {
break :blk bun.selfExePath() catch break :blk2;
}

View File

@@ -0,0 +1,62 @@
import { $ } from "bun";
import { expect, test } from "bun:test";
import { tempDir } from "harness";
// Regression test for https://github.com/oven-sh/bun/issues/25885
// Shell command resolution should respect PATH set via .env()
test("shell respects PATH set via .env() for command resolution", async () => {
using dir = tempDir("shell-path-env", {
"mytest": '#!/bin/bash\necho "hello from mytest"',
});
// Make the script executable
await $`chmod +x ${String(dir)}/mytest`.quiet();
const enhancedPath = `${String(dir)}:${process.env.PATH}`;
// Both `which` builtin and direct command execution should find the binary
const whichResult = await $`which mytest`.env({ ...process.env, PATH: enhancedPath }).quiet();
expect(whichResult.stdout.toString().trim()).toBe(`${String(dir)}/mytest`);
// This was the bug: direct execution failed even though `which` worked
const execResult = await $`mytest`.env({ ...process.env, PATH: enhancedPath }).quiet();
expect(execResult.stdout.toString().trim()).toBe("hello from mytest");
expect(execResult.exitCode).toBe(0);
});
test("shell respects PATH set via export for command resolution", async () => {
using dir = tempDir("shell-path-export", {
"mytest2": '#!/bin/bash\necho "hello from mytest2"',
});
// Make the script executable
await $`chmod +x ${String(dir)}/mytest2`.quiet();
const enhancedPath = `${String(dir)}:${process.env.PATH}`;
// Test with export (export_env)
const result = await $`export PATH=${enhancedPath}; mytest2`.quiet();
expect(result.stdout.toString().trim()).toBe("hello from mytest2");
expect(result.exitCode).toBe(0);
});
test("shell PATH lookup priority: cmd_local_env > export_env", async () => {
using dir1 = tempDir("shell-path-priority-1", {
"prioritytest": '#!/bin/bash\necho "from dir1"',
});
using dir2 = tempDir("shell-path-priority-2", {
"prioritytest": '#!/bin/bash\necho "from dir2"',
});
await $`chmod +x ${String(dir1)}/prioritytest`.quiet();
await $`chmod +x ${String(dir2)}/prioritytest`.quiet();
// cmd_local_env (PATH=x command) should take priority over export_env
const path1 = `${String(dir1)}:${process.env.PATH}`;
const path2 = `${String(dir2)}:${process.env.PATH}`;
// export sets export_env, then PATH=x sets cmd_local_env for that command
// cmd_local_env should win for command resolution
const result = await $`export PATH=${path2}; PATH=${path1} prioritytest`.quiet();
expect(result.stdout.toString().trim()).toBe("from dir1");
});