Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/runtime/shell.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ You can redirect output to these JavaScript objects:
- `Buffer`, `Uint8Array`, `Uint16Array`, `Uint32Array`, `Int8Array`, `Int16Array`, `Int32Array`, `Float32Array`, `Float64Array`, `ArrayBuffer`, `SharedArrayBuffer` (writes to the underlying buffer)
- `Bun.file(path)`, `Bun.file(fd)` (writes to the file)

A buffer target holds a fixed number of bytes. Output that fits is read to the end. When a command writes more than the buffer holds, the shell closes the command's output pipe. The command then gets `EPIPE`, `ECONNRESET`, or `SIGPIPE` on its next write and stops, the same as a writer whose reader has gone away.

### Example: Redirect input from JavaScript objects (`<`)

To use a JavaScript object as stdin, use the `<` operator:
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/shell/subproc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,10 @@ impl PipeReader {

#[allow(unused_mut)]
let mut reader = IOReader::init::<PipeReader>();
if let BufferedOutput::ArrayBuffer { buf, .. } = &buffered_output {
// The extra byte is clipped by `append`: that is how an overflow shows.
reader.set_limit(Some(buf.slice().len() + 1));
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
#[cfg(not(windows))]
let stdio_result = result;
#[cfg(windows)]
Expand Down
39 changes: 39 additions & 0 deletions test/js/bun/shell/bunshell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3167,6 +3167,45 @@ function sentinelByte(buf: Uint8Array): number {
throw new Error("No sentinel byte");
}

describe.concurrent("`> ${buf}` redirect of an external command that writes more than the buffer holds", () => {
// The shell reads the child's output into the buffer and stops at its end.
// The child then gets a write error (EPIPE or ECONNRESET) or SIGPIPE on its
// next write, the same as a writer whose reader closed the pipe. Bun ignores
// SIGPIPE, so this child sees the error and exits 7. Without the limit the
// shell drains and drops the rest of the output forever, and a child that
// never stops writing never settles the promise.
const endlessWriter = (fd: number) =>
`const chunk = Buffer.alloc(4096, "y"); const fs = require("fs"); try { for (;;) fs.writeSync(${fd}, chunk); } catch { process.exit(7); }`;

test("stdout: the child stops with a write error and the buffer is full", async () => {
const buffer = Buffer.alloc(16);
const result = await $`${BUN} -e ${endlessWriter(1)} > ${buffer}`.env(bunEnv).nothrow();
expect(buffer.toString()).toBe(Buffer.alloc(16, "y").toString());
expect(result.exitCode).toBe(7);
});

test("stderr: the child stops with a write error and the buffer is full", async () => {
const buffer = Buffer.alloc(16);
const result = await $`${BUN} -e ${endlessWriter(2)} 2> ${buffer}`.env(bunEnv).nothrow();
expect(buffer.toString()).toBe(Buffer.alloc(16, "y").toString());
expect(result.exitCode).toBe(7);
});

test("output that fits exactly is kept and the command exits 0", async () => {
const buffer = Buffer.alloc(16);
const code = `process.stdout.write(Buffer.alloc(16, "a"));`;
const result = await $`${BUN} -e ${code} > ${buffer}`.env(bunEnv).nothrow();
expect(buffer.toString()).toBe(Buffer.alloc(16, "a").toString());
expect(result.exitCode).toBe(0);
});

test("a zero-length buffer settles", async () => {
const buffer = Buffer.alloc(0);
const result = await $`${BUN} -e ${endlessWriter(1)} > ${buffer}`.env(bunEnv).nothrow();
expect(result.exitCode).toBe(7);
});
});

describe("interpolated values in assignment position", () => {
// An `=` that arrives via an interpolated template value is data, not shell
// syntax. The value must remain a single inert command word instead of being
Expand Down
Loading