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

Key ideas

Eight points cover how every part of proc behaves. The rest of the book is detail.

1. A pipeline is a source, some steps, and a consumer

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

const errors = await read("app.log") // a source: the file's bytes
  .lines // a step: bytes to lines of text
  .filter((line) => line.includes("ERROR")) // another step
  .count(); // the consumer: pulls everything through

console.log(errors);
2

A source produces items: run() (a command’s output), read() (a file), or enumerate() (any iterable or async iterable). Each step (lines, map, filter, take, transform, …) returns a new Enumerable and does nothing yet; .run() is the exception, since it starts its command at once (point 2). The consumer (collect, forEach, count, reduce, first, writeTo, toStdout, or a for await loop) pulls items through one at a time and returns a promise. Await it.

2. A command starts when you call run(), and you must read its output

run() starts the child process at once; only the steps after it wait. The child’s stdout is a pipe with a small buffer (typically 64 KB). A child that writes more than that waits until someone reads, so if your code never consumes the output, the child never finishes and neither does your program. Always end a command’s pipeline with a consumer. If you don’t want the output, consume it anyway: await run("make").lines.forEach(() => {}).

stderr is not piped by default: it goes straight to your terminal.

3. Errors come out of the consumer’s await

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

try {
  await run("sh", "-c", "echo one; echo two; exit 3")
    .lines
    .forEach((line) => console.log(line));
} catch (error) {
  if (error instanceof ExitCodeError) {
    console.log(`failed with exit code ${error.code}`);
  } else {
    throw error;
  }
}
one
two
failed with exit code 3

A command that exits with a non-zero code throws ExitCodeError once you have read all of its output, so the lines it wrote before failing still reach you. The same catch also gets an error from any command earlier in a pipeline (as an UpstreamError whose cause is the original), and anything your own callbacks throw. One try around the consumer covers the whole pipeline. See Errors.

4. Stopping early is fine

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

// `yes` prints "y" forever; taking three stops it.
const ys = await run("yes").lines.take(3).collect();
console.log(ys);
[ "y", "y", "y" ]

When a consumer stops before the end (take, first, find, a break out of for await), proc closes the pipeline behind it, and the await returns at once, without an error. A command that is still writing dies at its next write; one that runs on quietly, like a server that printed “ready”, keeps running until it exits, or until main() stops it on the way out. Deno doesn’t exit while a child is running, so without main() the script ends only when that child does.

5. An Enumerable is used once

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

const doubled = enumerate([1, 2, 3]).map((n) => n * 2);

console.log(await doubled.collect());
console.log(await doubled.collect()); // already used up: nothing left

const saved = await enumerate([1, 2, 3]).map((n) => n * 2).collect();
console.log(await enumerate(saved).count(), await enumerate(saved).count());
[ 2, 4, 6 ]
[]
3 3

Consuming an Enumerable uses it up, and a second pass finds nothing, without an error. To go over data twice, collect it into an array first, or split the stream with tee().

6. Some members are properties

.lines, .chunkedLines, .first, .status, and .pid take no parentheses. .first and .status are promises: await p.status, not p.status(). Everything else is a method.

7. Parsers yield batches

import { read } from "@j50n/proc";
import { fromCsvToRows } from "@j50n/proc/transforms";

const rows = await read("people.csv")
  .transform(fromCsvToRows()) // yields batches: arrays of rows
  .flatten() // one row at a time
  .collect();

console.log(rows);
[ [ "name", "city" ], [ "Ada", "London" ], [ "Grace", "Arlington" ] ]

For speed, the parsers in @j50n/proc/transforms yield arrays of rows rather than one row at a time. Add .flatten() before a step that works on one row. The row writers (toCsv(), toTsv(), toRecord()) take either; toJson() takes one value per item.

8. Wrap a long-running program in main()

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

await main(async () => {
  await run("./long-job.sh").lines.toStdout();
});

When Deno is told to stop (Ctrl-C, or a container’s SIGTERM), it exits at once without waiting for its children; in a container they are killed with it before they can clean up. When an error goes uncaught, Deno kills its children as it exits, on any host. main() runs your program, and however it ends (it returns, it throws, or a signal arrives), it makes sure every child has been asked to stop, and waits for them, up to 30 seconds, before exiting. See Shutting down cleanly.