Skip to content

jhheider/edikt

Repository files navigation

edikt

CI Coverage Status crates.io License

Edit config files without reflowing them.

You want to change one value in a commented, hand-formatted config. Today every option costs you something:

$ jq '.compilerOptions.target = "ES2022"' tsconfig.json
# comments gone, trailing commas gone, the whole file reindented,
# and JSONC-with-comments won't round-trip through jq at all

edikt changes the one value and nothing else:

$ edikt -i '.compilerOptions.target = "ES2022"' tsconfig.json
# one line of the diff moves; every comment, blank line, indent, and
# trailing comma comes back byte for byte

edit, meets edict. A lossless, format-preserving editor for JSONC/JSON5, TOML, YAML, INI, KDL, and flat key-value files (.env, .properties), driven by a jq-flavored expression language and a sed-shaped execution model. It touches only the bytes you point at.

# query - reads like jq
edikt '.compilerOptions.strict' tsconfig.json

# edit in place - comments, indent, comma style all preserved
edikt -i '.compilerOptions.target = "ES2022"' tsconfig.json

# edit YAML in place - anchors, flow style, and comments survive
edikt -i '.services.web.replicas = 3' compose.yaml

# compute, not just place
edikt -i '.version |= . + "-dev"' package.jsonc

# stream-first, like sed (stdin has no extension, so name the format)
cat settings.jsonc | edikt -t jsonc 'del(.telemetry) | .theme = "dark"'

# script from a file
edikt -f release.edk -i config.jsonc

# convert, where feasible - comments carried across, in the target's syntax
edikt -T yaml tsconfig.jsonc

# Markdown frontmatter - edit the metadata block, the prose untouched
edikt -i '.draft = false' post.md

# multi-document YAML - one edit maps over every ---document in the stream
edikt -i '.metadata.labels.env = "prod"' k8s.yaml

# ...or target one document by position with ^dN (0-based, strict)
edikt -i '^d1 | .spec.replicas = 3' k8s.yaml

# PEP 723 / scriptbox blocks - bump the pin, the code below untouched
edikt -t frontmatter -i '.["requires-python"] = ">=3.12"' app.py

Every one of these writes back a file that is byte-identical except for the value you changed, the kind of diff a reviewer reads at a glance.

Beyond config files: frontmatter and streams

Markdown frontmatter. For a .md/.markdown/.mdx/.qmd file (or -t markdown), edikt edits the leading metadata block, YAML (---), TOML (+++), tagged (---json), or Hugo bare-brace JSON, and leaves the document body byte-for-byte opaque. edikt '.title' post.md queries it; edikt -i '.draft = false' post.md rewrites one key with the prose intact.

Commented host-language blocks. edikt reads PEP 723 # /// script ... # /// blocks in a Python file (uv) or a shell script (scriptbox), bumps the pin, and re-applies the # prefix with the code below untouched.

Multi-document YAML. A ----separated stream (Kubernetes manifests, Ansible, Helm output) is first-class. An edit maps over every document by default and silently skips any that lack the target path; a query returns one result per document. select(pred) targets documents by content (select(.kind == "Service") | .spec.type = "LoadBalancer"), and ^dN selects one by position (^d0 is the first), strict there, so ^d5 on a three-document stream is an error, not a no-op.

Install

brew install jhheider/tap/edikt   # Homebrew
cargo install edikt               # Cargo (crates.io)
pkgx install edikt                # pkgx (builds from source)

Or grab a prebuilt binary (Linux, macOS, and Windows; x86_64 and arm64) from the releases page.

A worked example: bumping a Cargo workspace

edikt is happy editing the config it ships in. Here is the whole version bump for a release, a Cargo.toml per crate plus the internal dependency pins in the workspace root, done losslessly:

# each crate's own version
for c in crates/*/Cargo.toml; do
  edikt -i '.package.version = "1.2.3"' "$c"
done

# the [workspace.dependencies] pins (inline tables), chained with -e
edikt -i \
  -e '.workspace.dependencies."edikt-core".version   = "1.2.3"' \
  -e '.workspace.dependencies."edikt-syntax".version = "1.2.3"' \
  Cargo.toml

Each edit moves exactly one version = "...". The # Internal crates comment above the dependency table, the path = "crates/edikt-core" sitting next to each version, the brace-and-space style of every inline table, and every other byte come back unchanged, the kind of diff a reviewer reads at a glance.

One quoting note: a key containing - is written ."edikt-core" (or .["edikt-core"]), because a bare .edikt-core would read the - as subtraction. edikt says so with a clear error rather than guessing.

Why edikt?

Honestly, a weekend project, one that started with a real itch. The problem crystallized while reading "Respectful" YAML Patching in Rust, which surveys the Rust libraries for patching YAML and lands on the gap in one line: none of them preserve both the formatting and the comments. That's the exact thing I kept wanting, surgically change one value (or one comment) and leave every other byte, comment, and blank line alone, and not just for YAML but for the whole pile of config formats a project accumulates.

The good tools each own their corner: jq and yq for querying, taplo and prettier for formatting, and the excellent toml_edit and kdl-rs crates for lossless edits (edikt is built on those last two). edikt isn't trying to replace them, it's the piece I couldn't find off the shelf: one jq-flavored tool that edits and queries and converts across JSONC, INI, TOML, YAML, KDL, and .env, touching only the bytes you point at. If your need is single-format, reach for the specialist; if it's "the same surgical edit, across all of these," that's the gap this fills.

What edikt won't do

It edits values and comments; it does not reformat, lint, or validate schemas, reach for taplo/prettier for that. The expression language is a curated subset of jq (navigation, mutation, arithmetic, // defaults, regex, split/join); if/then, reduce, variables (as $x), and user-defined functions are not in v1. And .env is flat and string-valued, always. Stating the boundaries plainly is the point: inside them, the surgical-edit promise holds byte for byte.

Status

Seven config formats with lossless in-place edit, query, and comment-preserving conversion, plus a frontmatter lens (Markdown and PEP 723 host-language blocks) and multi-document YAML streams with select/^dN targeting. On crates.io, Homebrew, and pkgx, the badge above tracks the current version. See CLAUDE.md for the build contract.

Scripting notes

  • Exit codes are sed-shaped: 0 = success, a query that matches nothing is a silent no-op (safe under set -e); 2 = parse or evaluation error. For presence tests, --exit-status opts into jq's 1 on zero matches; for defaults, use //: edikt '.maybe.key // "fallback"' f.yaml.
  • The expression language is deliberately capped in v1. jq's navigation, mutation, arithmetic, // defaults, and a curated builtin registry (including regex test/match/capture/sub/gsub, split/join) are in; variables (as $x), if/then, reduce, and user-defined functions are not (yet).
  • Many files, sed-style: edikt -i '.v = 9' a.json b.json (let the shell glob: edikt -i 'del(.telemetry)' config/*.jsonc). Queries over several files concatenate results in order.
  • .env is flat and string-valued, no arrays or nesting, ever, but string computation on values works fine: edikt -i '.VERSION |= sub("^v"; "")' .env.

License

Licensed under either of Apache License, Version 2.0 or MIT license, at your option.

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages