Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
b965ffd97a fix(shell): use PATH from .env() for command resolution
When using the bun shell `$` template literal, setting PATH via `.env()`
was not affecting command resolution. The `which` builtin correctly saw
the new PATH, but actual command execution failed to find binaries.

The issue was that `SpawnArgs.default()` initialized PATH from the
process environment, and command resolution via `which()` happened
before `fillEnv()` was called to update PATH from the shell's
`export_env` and `cmd_local_env`.

The fix checks the shell environment for PATH before falling back to
the process PATH, matching the behavior of the `which` builtin.

Fixes #25855

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 19:49:03 +00:00
2 changed files with 69 additions and 1 deletions

View File

@@ -485,7 +485,13 @@ 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 takes precedence over export_env (more local scope wins).
const path_key = shell.EnvStr.initSlice("PATH");
const shell_path = this.base.shell.cmd_local_env.get(path_key) orelse
this.base.shell.export_env.get(path_key);
const lookup_path = if (shell_path) |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 { describe, expect, test } from "bun:test";
import { bunEnv, tempDir } from "harness";
describe("issue #25855 - shell .env() PATH should affect command resolution", () => {
test("command resolution uses PATH from .env()", async () => {
// Create a temp directory with a test executable
using dir = tempDir("bun-path-test", {});
const binDir = `${dir}/bin`;
await $`mkdir -p ${binDir}`.quiet();
// Create a simple test script
const testBinary = `${binDir}/mytest25855`;
await Bun.write(testBinary, '#!/bin/bash\necho "hello from mytest25855"');
await $`chmod +x ${testBinary}`.quiet();
// Create enhanced PATH with our bin directory prepended
const enhancedPath = `${binDir}:${process.env.PATH}`;
// Test: Direct command execution should find the binary via enhanced PATH
const execResult = await $`mytest25855`.env({ ...bunEnv, PATH: enhancedPath }).quiet();
expect(execResult.stdout.toString().trim()).toBe("hello from mytest25855");
expect(execResult.exitCode).toBe(0);
});
test("which builtin and command execution use same PATH", async () => {
// Create a temp directory with a test executable
using dir = tempDir("bun-path-test", {});
const binDir = `${dir}/bin`;
await $`mkdir -p ${binDir}`.quiet();
// Create a simple test script
const testBinary = `${binDir}/whichtest25855`;
await Bun.write(testBinary, '#!/bin/bash\necho "found me"');
await $`chmod +x ${testBinary}`.quiet();
// Create enhanced PATH
const enhancedPath = `${binDir}:${process.env.PATH}`;
const envWithPath = { ...bunEnv, PATH: enhancedPath };
// Both which and direct execution should work with the same PATH
const whichResult = await $`which whichtest25855`.env(envWithPath).quiet();
expect(whichResult.stdout.toString().trim()).toBe(testBinary);
const execResult = await $`whichtest25855`.env(envWithPath).quiet();
expect(execResult.stdout.toString().trim()).toBe("found me");
});
test("command not in custom PATH still fails appropriately", async () => {
// Use a PATH that doesn't include standard directories
using dir = tempDir("bun-path-test", {});
const emptyPath = String(dir);
// Note: ls, cat, echo, etc. are shell builtins in bun, so they'll work without PATH.
// We use 'node' as an example of a real external command that won't be a builtin.
const result = await $`node --version`
.env({ ...bunEnv, PATH: emptyPath })
.nothrow()
.quiet();
expect(result.exitCode).not.toBe(0);
});
});