Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Running a command

import { run } from "@j50n/proc";

const lines = await run("head", "-n", "3", "app.log").lines.collect();

console.log(lines);
[
  "2026-10-05 09:00:01 INFO  server started on :8080",
  "2026-10-05 09:00:07 INFO  GET /health 200",
  "2026-10-05 09:01:13 ERROR db: connection refused"
]

run() takes the program and its arguments as separate strings. No shell is involved, so nothing needs quoting, and run("head -n 3 app.log") looks for a program with that whole name. The program is looked up on PATH, or given as a path or a file URL. A relative path such as ./build.sh is found from the child’s working directory, so with the cwd option below it is looked for in cwd, not where your program runs.

The child starts at the call. run() returns a ProcessEnumerable: the child’s stdout as an async iterable of bytes, with all the Enumerable methods. Steps such as .lines wait until a consumer pulls; the process doesn’t. If the command fails, the consumer’s await throws (see Errors).

Reading the output

import { concat, run } from "@j50n/proc";

// Every line, in an array.
const all = await run("cat", "fruit.txt").lines.collect();
console.log(all);

// One line at a time, as the command writes them.
for await (const line of run("cat", "fruit.txt").lines) {
  console.log(`- ${line}`);
}

// Only the first line.
const first = await run("cat", "fruit.txt").lines.first;
console.log(first);

// Raw bytes, in chunks as they arrive; `concat` joins them.
const chunks = await run("cat", "app.log.gz").collect();
console.log(`${concat(chunks).length} bytes`);

// Straight to this program's stdout, unchanged.
await run("cat", "fruit.txt").toStdout();
[ "cherry", "apple", "banana" ]
- cherry
- apple
- banana
cherry
176 bytes
cherry
apple
banana
  • .lines decodes UTF-8 and splits on "\n" (a "\r" before it goes too). It is a property, not a method.
  • .collect() gathers everything into an array. For large output, use for await or .forEach() instead, which handle one line at a time and keep nothing.
  • .first is a promise of the first line. It stops reading there, which ends the command early (see Pipelines), and throws RangeError if there is no output at all.
  • Without .lines the items are Uint8Array chunks of whatever size the pipe delivered, not lines. concat() joins them.
  • .toStdout() copies the output to your program’s stdout. Use it when you only want the user to see it.

The output can be read once (Key ideas): a second pass finds nothing, and doesn’t throw a failed command’s error again.

Working directory and environment

Options go before the command:

import { run } from "@j50n/proc";

await Deno.mkdir("reports", { recursive: true });
await Deno.writeTextFile("reports/q3.txt", "");

const line = await run(
  { cwd: "reports", env: { REGION: "west" } },
  "sh",
  "-c",
  'echo "$REGION: $(ls)"',
).lines.first;

console.log(line);
west: q3.txt

cwd sets the child’s working directory. env adds variables to the environment the child inherits, or overrides them; it can’t remove one. A PATH in env also changes where the program is looked up, so run({ env }, "ls") runs whatever ls comes first on that PATH. Don’t build env from untrusted input. clearEnv: true starts the child with only env, to keep secrets in your environment from reaching it. proc still finds the program on your PATH, but the child gets none unless env has one, so put a PATH in env if it starts other programs by name.

timeoutMs stops a child that runs too long: proc sends it SIGTERM, and reading its output throws a TimeoutError (Errors). The other options, fnStderr and fnError, are about errors and are covered in Errors. All of them are listed under ProcessOptions.

Status and PID

import { run } from "@j50n/proc";

// No output to read, so waiting on the status alone is safe. It doesn't
// throw for a failed exit; it reports it.
const { success, code } = await run("test", "-e", "missing.txt").status;
console.log(success, code);

// With output: read it first, then look at the status.
const p = run("sh", "-c", "echo hello");
console.log(p.pid > 0);
console.log(await p.lines.collect());
console.log((await p.status).success);
false 1
true
[ "hello" ]
true

.status resolves when the child exits, with success, code, and signal. It never throws for a failed exit; it tells you about it. .pid is the child’s process ID, available as soon as run() returns.

Waiting for a command you don’t want output from

.status resolves only when the child exits, and a child that writes more than its stdout pipe holds (typically 64 KB) can’t exit until someone reads. This fragment hangs forever, because nothing reads seq’s output:

await run("seq", "1", "100000").status; // hangs: nobody reads the output

So read the output even when you don’t want it:

import { run } from "@j50n/proc";

// `seq` writes about 600 KB, far more than the pipe holds. Reading the output
// and throwing it away lets it finish, and a failure would still throw.
await run("seq", "1", "100000").forEach(() => {});

console.log("done");
done

Calling .forEach() on the raw bytes skips decoding them, and a failed exit still throws. Use .status alone only for a command that prints little or nothing, such as test or grep -q, and when a failed exit is an answer rather than an error.

stderr goes to your terminal

import { run } from "@j50n/proc";

const out = await run("sh", "-c", "echo to stdout; echo to stderr >&2")
  .lines
  .collect();

console.log(out);
[ "to stdout" ]

Only stdout is captured. The child’s stderr is connected straight to your program’s, so to stderr appeared on the terminal and isn’t in the result. To capture it, pass fnStderr; see Capturing stderr.

The child’s stdin is closed: a program that reads stdin sees end of input at once. To feed it data, pipe into it with .run(), as Pipelines and input shows.

A program that isn’t there

If the program doesn’t exist, run() itself throws Deno.errors.NotFound, before any consumer runs. Keep the run() call inside the same try as the await, and it is caught with everything else; see Errors.

Permissions

Running a command needs --allow-run, or --allow-run=head,sort to allow only those programs. Passing env doesn’t need --allow-env. See Install.