Files and standard streams
read(path) gives a file’s bytes as an Enumerable, and writeTo(path) writes
bytes to a file. In between, the data streams a chunk at a time.
import { read } from "@j50n/proc";
// Copy the error lines into a file of their own.
await read("app.log")
.lines
.filter((line) => line.includes("ERROR"))
.writeTo("errors.txt"); // each line written with a "\n"
console.log(await read("errors.txt").lines.collect());
[
"2026-10-05 09:01:13 ERROR db: connection refused",
"2026-10-05 09:02:45 ERROR GET /orders/17 500"
]
read() opens the file when reading starts, not at the call, and closes it when
reading ends, including when the consumer stops early. A missing file throws
Deno.errors.NotFound from the consumer’s await. readLines(path) is
shorthand for read(path).lines.
Writing a file
writeTo(path) creates the file, or replaces what it held, and closes it when
the sequence ends. It writes items as toStdout() does: bytes as they are, and
each string as a line, with a "\n" added. If the source throws, including a
command in the pipeline that fails, the file is closed holding what was written
so far, and the error comes out of writeTo. The file is emptied before
anything is read, so what it held before is gone either way, and a pipeline that
reads the same file (read(path) … writeTo(path)) finds it already empty,
as cmd < f > f does in a shell.
To replace a file only once everything has worked, or to rewrite one in place,
pass { atomic: true }:
import { read } from "@j50n/proc";
// Rewrite fruit.txt in place: atomic writes a new file and renames it over
// the old one, so reading the old one while writing works.
await read("fruit.txt")
.lines
.map((line) => line.toUpperCase())
.writeTo("fruit.txt", { atomic: true });
console.log((await Deno.readTextFile("fruit.txt")).trimEnd());
CHERRY
APPLE
BANANA
With atomic, proc writes a new file beside the old one, flushes it to disk,
and renames it into place once everything is written. A failure leaves the old
file as it was, with nothing beside it, and so does a program that exits partway
under main. A symlink stays a symlink, and the file keeps its mode. Anything
reached through /dev or /proc, such as /dev/stdout, is written in place.
It needs read permission on the file and write permission on its directory, and
the result is a new file, so a hard link to the old one still shows the old
content.
To add to a file instead of replacing it, open it yourself and pass its
writable:
import { enumerate, toBytes } from "@j50n/proc";
// writeTo(path) replaces the file. To add to it, open it yourself.
await enumerate(["first run"]).transform(toBytes).writeTo("notes.txt");
const file = await Deno.open("notes.txt", { append: true });
await enumerate(["second run"]).transform(toBytes).writeTo(file.writable);
console.log(JSON.stringify(await Deno.readTextFile("notes.txt")));
"first run\nsecond run\n"
writeTo(stream) takes any WritableStream and closes it at the end, which
closes the file too.
stdout
import { enumerate, read, toBytes } from "@j50n/proc";
// Strings are written as lines; bytes as they are.
await enumerate(["one", "two"]).toStdout();
await read("fruit.txt").toStdout();
// writeTo() closes what it writes to, unless told not to.
await enumerate(["three"])
.transform(toBytes)
.writeTo(Deno.stdout.writable, { noclose: true });
console.log("stdout is still open");
one
two
cherry
apple
banana
three
stdout is still open
toStdout() writes each string with a "\n" added, and bytes as they are, and
leaves stdout open. Use it for a command’s output too: run("ls").toStdout().
When stdout’s reader goes away before the program is done writing, as when its
output is piped into head, toStdout() stops as a consumer that stops early
does: it closes the source and resolves, without an error.
writeTo(Deno.stdout.writable) works, but closes stdout when it finishes unless
you pass { noclose: true }. After that, every console.log in the program
throws BadResource.
stdin
Deno.stdin.readable is a stream of bytes, so enumerate() wraps it like any
other source:
import { enumerate } from "@j50n/proc";
// Print the lines of stdin that mention ERROR.
await enumerate(Deno.stdin.readable)
.lines
.filter((line) => line.includes("ERROR"))
.toStdout();
deno run stdin-errors.ts < app.log
2026-10-05 09:01:13 ERROR db: connection refused
2026-10-05 09:02:45 ERROR GET /orders/17 500
Reading stdin needs no permission. A child process started with run() gets no
stdin by default; to feed it, pipe into it with .run()
(Pipelines and input).
Compressed files
CompressionStream and DecompressionStream take byte chunks, so the bytes
from read(), a command’s output, or toBytes go straight in:
import { enumerate, gunzip, gzip, read } from "@j50n/proc";
// Bytes go straight into the web streams.
const errors = await read("app.log.gz")
.transform(new DecompressionStream("gzip"))
.lines
.count((line) => line.includes("ERROR"));
console.log(`${errors} errors`);
await read("app.log")
.transform(new CompressionStream("gzip"))
.writeTo("copy.log.gz");
// gzip and gunzip also take lines of text.
await enumerate(["alpha", "beta"]).transform(gzip).writeTo("words.gz");
console.log(await read("words.gz").transform(gunzip).lines.collect());
2 errors
[ "alpha", "beta" ]
The gzip and gunzip steps do the same, and also accept lines of text, which
they turn into bytes first. Data that isn’t valid gzip throws TypeError.
.run("gzip", "-dc") works too, in a child process.
Lines or bytes
.lines decodes UTF-8 and splits on "\n", dropping a "\r" before it, so
CRLF files work. Invalid UTF-8 throws TypeError. Keep the bytes for anything
that isn’t text: compressed data, images, or a copy that must be exact.
toByteLines splits bytes into lines without decoding them. For files with a
great many short lines, .chunkedLines gives the same lines an array at a time,
and is several times faster.
Large files
A chain that streams from read() to writeTo() holds only a few chunks at a
time, whatever the size of the file: filtering a 1.2 GB log through gzip peaked
at 150 MB of process memory, no more than a 300 MB log took. Two things break
that: collect(), which keeps every item, and a single line so long that it
doesn’t fit, since .lines holds a line until it ends. Count, reduce, or write
as you go instead of collecting.