Directory trees, made machine-readable — the missing half of
json2dir.
json2dir turns a JSON object into a directory tree. dir2json walks a
directory tree and prints the JSON object that reproduces it. Feed one into
the other and you get a lossless roundtrip.
Given this directory tree:
.
├── greeting # regular file, "Hello, world!"
├── dir
│ ├── subfile # regular file, "Content.\n"
│ └── subdir # empty directory
├── symlink -> target path
└── script # executable, "#!/bin/sh\necho Howdy!"
running
dir2json .
prints:
{
"greeting": "Hello, world!",
"dir": {
"subfile": "Content.\n",
"subdir": {}
},
"symlink": ["link", "target path"],
"script": ["script", "#!/bin/sh\necho Howdy!"]
}json2dir is write-only. You can materialize a tree from JSON, but the
moment you need to know what is actually on disk, you are on your own.
dir2json exists because the read direction is where most of the value is:
- Snapshot and review existing trees. Point
dir2jsonat/etc/something, a dotfiles directory, a rendered config tree — commit the output. Tree changes become ordinary line diffs in git, reviewable in an MR, instead ofdiff -routput or tarballs.json2diralone can only ever give you the first write of a tree; it has no way to capture one that already exists. - Drift detection, built in. The JSON document is your desired state;
dir2json's output is the observed state.dir2json --check desired.json /etc/mytreecompares the two, prints a path-level mismatch report and exits 3 — a machine-checkable conformance test in a single command, nodiffplumbing required. This is exactly the pattern configuration management is built on, now available for anything expressible as files. - Roundtrip verification.
dir2json | json2dirgives you a closed loop: apply, re-read, compare. If you manage trees withjson2dir, you can finally prove the materialization succeeded instead of trusting it. - Comparing hosts. Run it on two machines and diff the JSON. Because output is deterministic — entries sorted by name, no mtimes, no permissions noise, no inode ordering — the only differences you will see are real content differences.
- Refuses to lie. Non-UTF-8 contents, non-UTF-8 names, FIFOs, sockets
and devices are rejected loudly with a precise error message instead of
being silently mangled into a string that roundtrips into garbage. If you
genuinely have special files in the tree,
--skip-specialskips them with a warning — the choice is explicit, never silent. - Cycle-proof by construction. Symlinks are recorded as
["link", target]and never followed (the root being the only deliberate exception), so no symlink loop, self-reference, or symlinked/usrcan turn the walk into an infinite traversal. No depth limits needed.
In short: json2dir is the compiler, dir2json is the disassembler and
the test suite. A write-only tool asks you to trust it; this one lets you
check.
Inherited verbatim from json2dir, read in the opposite direction:
- Objects represent directories. Keys are entry names, values are the entries themselves.
- Strings represent the contents of regular files.
- Arrays with the first element
"link"represent symlinks; the second element is the target, recorded verbatim. - Arrays with the first element
"script"represent executable regular files; the second element is the contents.
Behavioral notes:
- Symlinks are never followed, only their targets are recorded. This
includes symlinks to directories — they become
["link", ...], not nested objects. The root is the single exception: if the root path itself is a symlink to a directory, it is followed. - Deterministic output: directory entries are sorted by name.
- Non-UTF-8 contents, names, or link targets are rejected with an error (regular JSON constraints apply).
- Special files (FIFOs, sockets, devices) are rejected by default;
pass
--skip-specialto skip them with a warning instead. - Permissions: everything except the executable bit is ignored — the scheme cannot represent it.
Usage: dir2json [OPTIONS] [DIR]
Arguments:
[DIR] Root directory to read [default: .]
Options:
-c, --compact Compact output instead of pretty-printed
-s, --skip-special Skip special files (FIFOs, sockets, devices) with a
warning instead of failing
--check FILE Compare DIR against the desired tree in FILE (a JSON
object, "-" for stdin) instead of printing; exits 3
with a mismatch report on drift
-h, --help Print help
-V, --version Print version
Exit status: 0 on success (or a matching --check), 1 on conversion
errors, 2 on usage errors, 3 on drift — so CI pipelines can tell "the
tree is bad", "the command line is bad" and "the machine drifted" apart.
A drift check is a single command:
$ dir2json --check desired.json /etc/mytree
drift detected: 2 mismatches
dir/subfile: missing on disk
greeting: expected "Hello, world!", got "goodbye"
$ echo $?
3
Or drive it from a pipeline — dir2json -c . | dir2json --check - .
re-asserts a tree against itself (and always passes, but now you know the
plumbing works).
cargo install --git https://github.com/71g3pf4c3/dir2json
nix profile install github:71g3pf4c3/dir2json
cargo build, cargo test, or nix build. A devShell with the Rust
toolchain is provided: nix develop.
The test suite covers three layers: unit tests for the tree walker and the
drift differ, integration tests for the CLI (exit codes, --check report
format, stdin mode), and a roundtrip test that materializes a tree the way
json2dir does and asserts that dir2json reproduces the original JSON
exactly. CI runs cargo fmt --check, cargo clippy -D warnings, cargo test and nix flake check on every push.
GPL-3.0-or-later.