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
.linesdecodes 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, usefor awaitor.forEach()instead, which handle one line at a time and keep nothing..firstis a promise of the first line. It stops reading there, which ends the command early (see Pipelines), and throwsRangeErrorif there is no output at all.- Without
.linesthe items areUint8Arraychunks 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.