Write a parser

On this page

This is the walkthrough for a pull request that adds lsof support, but the same steps apply to any command. Nothing here requires Go.

1. Capture output #

$ LC_ALL=C lsof -p $$ | head -20 > lsof.txt

Capture from the real implementation you are describing and record what it was (lsof 4.95 on Ubuntu 24.04). If the command prints machine-specific values (host names, users), replace them consistently; do not change the layout. Formats you cannot run yourself (another OS) need a fixture whose origin you can state; CI additionally runs the real command on Linux and macOS runners (e2e/atago/exec_*.atago.yaml).

2. Decide the variant #

Look at what differs between implementations and options. Each output format is one variant:

  • same columns everywhere → one variant (posix, default)
  • Linux vs macOS headers differ → linux / bsd (or darwin)
  • an option changes the columns → gnu / gnu-human

Name the directory registry/parsers/lsof/<variant>/.

3. Write the definition #

Start from the closest existing definition:

ShapeExample
header + rows, last column may contain spacesregistry/parsers/ps/unix/parser.yaml
header + rows with empty cellsregistry/parsers/lsblk/linux/parser.yaml
row label in the first columnregistry/parsers/free/gnu/parser.yaml
one regex per lineregistry/parsers/mount/linux/parser.yaml
one object from the whole outputregistry/parsers/uptime/linux/parser.yaml
nested valuesregistry/parsers/id/posix/parser.yaml
summary line + tableregistry/parsers/w/linux/parser.yaml

For lsof, the header is COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME and NAME may contain spaces, so a whitespace table with explicit columns fits:

format: 1
command: lsof
variant: default
description: lsof default columns
metadata:
  compatible: [lsof]
  references: [https://github.com/lsof-org/lsof]
detect:
  signature:
    all: ['^COMMAND\s+PID\s+USER\s+FD\s+TYPE\s+DEVICE\s+SIZE/OFF\s+NODE\s+NAME\s*$']
parse:
  type: table
  header:
    columns: [command, pid, user, fd, type, device, size_off, node, name]
  min_fields: 8
fields:
  pid: {type: int}

Always add a detect.signature, even for a single variant: it is what lets jz reject unrelated text with a clear message instead of producing nonsense. If the shape is too generic to be evidence on its own (a number and a path, three numbers in a row), add auto_detect: false next to it: the definition is then used only when the parser is named, and its signature is still checked there.

4. Add fixtures #

registry/parsers/lsof/default/testdata/lsof-4.95.txt
registry/parsers/lsof/default/testdata/lsof-4.95.yaml

The .yaml file documents the capture and feeds the selection test:

source: captured on Ubuntu 24.04 (lsof 4.95) with LC_ALL=C; user names replaced
os: linux
args: [-p, "1234"]

To pin text the definition must refuse, add a fixture with expect_error: <substring> and no .json. It covers both ways a definition refuses: the signature not describing the text, and the parse failing on it. The substring is matched against whichever message comes out, so expect_error: 'does not describe this input' pins a signature that keeps a neighbouring format away.

5. Generate the golden file and review it #

$ make registry-update-golden
$ git diff --stat registry/parsers/lsof
$ cat registry/parsers/lsof/default/testdata/lsof-4.95.json

Check types, nulls and the last column. A rounded, human-readable number stays a string: the output does not record the base and the value is already rounded, so converting it would invent precision. Then:

$ make test

The golden test fails if the fixture does not select its own variant (signature overlap with another variant), does not parse, or differs from the JSON.

6. Try it for real #

$ go run ./cmd/jz run lsof -p $$
$ lsof -p $$ | go run ./cmd/jz --pretty

7. Open the pull request #

Include the definition, the fixtures, the golden JSON and one row in the table in website/content/parsers.md. Use a parser: commit prefix.

Working outside the repository #

The same loop works with a personal registry and the released binary:

$ mkdir -p ~/.config/jsonize/registry/parsers/lsof/default/testdata
$ $EDITOR ~/.config/jsonize/registry/parsers/lsof/default/parser.yaml
$ make registry-update-golden DIR=~/.config/jsonize/registry
$ make registry-test DIR=~/.config/jsonize/registry

jz list shows the definition with source user, and it shadows an official definition of the same command and variant.