diff --git a/src/runtime/shell/builtin/cd.rs b/src/runtime/shell/builtin/cd.rs index bd000954d397..3945ba0e195a 100644 --- a/src/runtime/shell/builtin/cd.rs +++ b/src/runtime/shell/builtin/cd.rs @@ -121,9 +121,13 @@ impl Cd { interp: &Interpreter, cmd: NodeId, _: usize, - _err: Option, + err: Option, ) -> Yield { Self::state_mut(interp, cmd).state = State::Done; - Builtin::done(interp, cmd, 1) + Builtin::done( + interp, + cmd, + err.map_or(1, |e| e.get_errno() as crate::shell::ExitCode), + ) } } diff --git a/src/runtime/shell/builtin/echo.rs b/src/runtime/shell/builtin/echo.rs index 625863a9b7af..af5b3c888adc 100644 --- a/src/runtime/shell/builtin/echo.rs +++ b/src/runtime/shell/builtin/echo.rs @@ -113,7 +113,7 @@ impl Echo { Builtin::done( interp, cmd, - err.map(|e| e.errno as crate::shell::ExitCode).unwrap_or(0), + err.map_or(0, |e| e.get_errno() as crate::shell::ExitCode), ) } } diff --git a/src/runtime/shell/builtin/export.rs b/src/runtime/shell/builtin/export.rs index 29d6b0e5c71b..f936902fb450 100644 --- a/src/runtime/shell/builtin/export.rs +++ b/src/runtime/shell/builtin/export.rs @@ -82,6 +82,10 @@ impl Export { err: Option, ) -> Yield { Self::state_mut(interp, cmd).state = State::Done; - Builtin::done(interp, cmd, err.map_or(0, |_| 1)) + Builtin::done( + interp, + cmd, + err.map_or(0, |e| e.get_errno() as crate::shell::ExitCode), + ) } } diff --git a/src/runtime/shell/builtin/mv.rs b/src/runtime/shell/builtin/mv.rs index 915d34c3bfaf..4c148c27e123 100644 --- a/src/runtime/shell/builtin/mv.rs +++ b/src/runtime/shell/builtin/mv.rs @@ -330,7 +330,7 @@ impl Mv { _ => unreachable!(), }; // The failing rename's errno becomes the shell exit code. - let exit_code = e.errno as ExitCode; + let exit_code = e.get_errno() as ExitCode; let buf = Builtin::task_error_to_string(interp, cmd, Kind::Mv, &e).to_vec(); Self::write_failing_error(interp, cmd, &buf, exit_code).run(interp); return; diff --git a/src/runtime/shell/builtin/which.rs b/src/runtime/shell/builtin/which.rs index 9d53713b2fb8..83b0a644881e 100644 --- a/src/runtime/shell/builtin/which.rs +++ b/src/runtime/shell/builtin/which.rs @@ -179,7 +179,7 @@ impl Which { e: Option, ) -> Yield { if let Some(err) = e { - return Builtin::done(interp, cmd, err.errno as crate::shell::ExitCode); + return Builtin::done(interp, cmd, err.get_errno() as crate::shell::ExitCode); } match Self::state_mut(interp, cmd).state { State::OneArg => Builtin::done(interp, cmd, 1), diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 2d57ca0587c9..db1aa2586978 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -8,7 +8,17 @@ import { $ } from "bun"; import { afterAll, beforeAll, describe, expect, it, test } from "bun:test"; import { chmodSync, mkdirSync } from "fs"; import { mkdir, rm, stat } from "fs/promises"; -import { bunExe, isPosix, isWindows, rss, runWithErrorPromise, tempDir, tempDirWithFiles, tmpdirSync } from "harness"; +import { + bunExe, + isLinux, + isPosix, + isWindows, + rss, + runWithErrorPromise, + tempDir, + tempDirWithFiles, + tmpdirSync, +} from "harness"; import { join, sep } from "path"; import { createTestBuilder, sortedShellOutput } from "./util"; const TestBuilder = createTestBuilder(import.meta.path); @@ -89,6 +99,19 @@ describe("bunshell", () => { .runAsTest(cmdstr) : "", ); + + // Every write to /dev/full fails with ENOSPC. The builtin reports that + // errno as its exit code, like the builtins that already do (cat, rm). + test.skipIf(!isLinux)("builtins exit with the errno of a failed output write", async () => { + const ENOSPC = 28; + const exitCodes = { + echo: (await $`echo hi > /dev/full`.nothrow()).exitCode, + which: (await $`which sh > /dev/full`.nothrow()).exitCode, + export: (await $`export FOO=bar; export > /dev/full`.nothrow()).exitCode, + cd: (await $`cd /nonexistent-dir 2> /dev/full`.nothrow()).exitCode, + }; + expect(exitCodes).toEqual({ echo: ENOSPC, which: ENOSPC, export: ENOSPC, cd: ENOSPC }); + }); }); describe("concurrency", () => {