An Odin port of jc
(Kelly Brazil) — converts the output of common Unix commands and many text/file
formats into JSON.
- 150 parsers ported from the original 240
- 514 fixture-based tests pass against the upstream
jcreference outputs (0 failing) - Built and tested with Odin
dev-2026-04on macOS arm64
odin build src -out:jc
./jc --list-parsers
echo "127.0.0.1 localhost" | ./jc --hosts -p
free | ./jc --free
uname -a | ./jc --uname -pFlags:
| flag | meaning |
|---|---|
--<parser-name> |
choose a parser (e.g. --free) |
-p |
pretty-print |
-r |
raw output (skip post-processing) |
--list-parsers |
list every registered parser |
--about |
runtime info as JSON |
-h/--help |
usage |
The test harness compares each parser's output against the upstream jc JSON
fixtures so we can prove behavioural parity.
./run_tests.sh # build + run every fixture pairIt reads test cases from tests/cases*.tsv (one shard per implementation batch).
Each row points at a .out (raw input) and .json (expected output) file from
the upstream jc test fixtures. Comparison is semantic JSON equality
(via python3 -c "import json; …"), so insertion order in objects matters but
whitespace does not.
To run the suite you need the upstream jc repository checked out as a sibling
directory at ../jc/ (or anywhere — the absolute paths in cases.tsv files
point at /Users/... today; adjust if you relocate).
src/
main.odin entry, argv, --flags, stdin slurp
json.odin Value union (bool, i64, u64, f64, string, [dynamic]Value, ^Object),
encoder + minimal decoder
registry.odin ParserInfo + register_parser
registry_all.odin master register_all() that calls every batch
registry_batch_*.odin one shard per implementation batch
util.odin line/word splitters, simple_table_parse, sparse_table_parse,
convert_to_int, convert_size_to_int, etc.
parser_<name>.odin one parser per file
tests/
cases*.tsv parser_flag<TAB>input_path<TAB>expected_path
run_tests.sh build + diff harness
AGENT_SPEC.md conventions used during the multi-agent port
- System info: env, hosts, uname, lsb-release, os-release, free, uptime
- Identity / accounts: id, group, passwd, shadow, gshadow, hash, hashsum, cksum
- Filesystem / processes: df, du, mount, ps, lsof, jobs, fstab, kv, kv-dup, pgpass, ls, wc, find, findmnt, lsattr, file, stat (limited), swapon, lsblk, zipinfo
- Networking: ifconfig, route, arp, host, blkid, ping, traceroute, tracepath, ip-route, iwconfig, wg-show, ufw-appinfo, dmidecode, lspci, certbot
- Service / time: systemctl (and
-lj/-ls/-lufvariants), ntpq, time, zpool-iostat, zpool-status, timedatectl, timezone - Text formats: ini, csv, m3u, semver, email-address, path, path-list
- Hardware: lsblk, lspci, dmidecode, efibootmgr, amixer, airport, udevadm, chage
- Linux
/proc/*: 33 parsers covering loadavg, meminfo, cpuinfo, version, modules, filesystems, buddyinfo, consoles, crypto, devices, diskstats, mtrr, vmstat, stat, swaps, zoneinfo, driver-rtc, interrupts, iomem, ioports, locks, partitions, pagetypeinfo, slabinfo, softirqs, vmallocinfo, plus net-{arp,dev,dev-mcast,if-inet6,igmp,igmp6,ipv6-route,netlink,netstat,packet,protocols,route,tcp,unix} and pid-{io,statm,stat,fdinfo,maps,mountinfo,numa-maps,smaps,status} - Misc commands: acpi, debconf-show, dpkg-l, lsmod, last, history, sysctl, finger, w, who, cbt, veracrypt, nsd-control, iostat, mpstat, vmstat, ssh-conf, apt-cache-show, apt-get-sqq, crontab, crontab-u, pip-list, pip-show, iptables, needrestart, resolve-conf, update-alt-q, update-alt-gs
Run ./jc --list-parsers to see every registered parser.
These parsers exist upstream but are not ported here — typically because they need substantial library support that Odin core does not provide, or their fixtures embed timezone-dependent timestamps that cannot be reproduced exactly on a different machine.
| Reason | Examples |
|---|---|
| Heavy stdlib dependency | url, ip-address (Python urllib.parse/ipaddress) |
| Format parsers Odin core lacks | xml, yaml, plist, toml |
| asn1 / cryptographic | x509-cert, x509-csr, x509-crl, jwt |
TZ-dependent *_epoch fields |
date, stat, mdadm, certbot-certs (some), tune2fs, rpm-qi |
| Very large / polymorphic | netstat, ss, lsusb, top, iftop, hciconfig |
| Heavy regex/state machines | dig, nmcli, ufw (ruleset), cef, clf, http-headers, xrandr |
Sub-format wrappers (*_s streaming) |
csv-s, cef-s, ping-s, top-s, syslog-s, … |
PRs filling these are welcome.
A parser is a single proc:
ParserProc :: proc(data: string, raw: bool) -> ValueEvery parser file does two things:
package main
register_<name> :: proc() {
register_parser(ParserInfo{
name = "<flag>", // "--<flag>" on the CLI
description = "...",
tags = []string{"command"},
parse = parse_<name>,
})
}
parse_<name> :: proc(data: string, raw: bool) -> Value {
// mirror jc/jc/parsers/<name>.py exactly
}Registration is centralised in registry_all.odin, which calls one
register_batch_<id>() per implementation batch. Each batch lives in its own
shard so multiple developers (or agents) can extend the project without
stepping on each other's file edits.
The test harness, the AGENT_SPEC briefing, and the batch shards together made it practical to do most of this port via parallel agents fanning out from a small hand-written core.
- Original
jc© Kelly Brazil — MIT license. https://github.com/kellyjonbrazil/jc - The fixtures used to drive the port live in the upstream
jcrepository'stests/fixtures/directory and are not redistributed here.
MIT, matching upstream jc.