Skip to main content

knf

Merges layered configuration files and prints the result. One job, no query language, no template engine.

# Print the output to stdout
knf base.toml prod.toml > merged.toml
# Add manual overrides via the --set flag
knf defaults.json overrides.json --set server.port=8080 --set host=name
# Mix toml and json (if you want)
knf *.toml *.json

It exists because more powerful alternatives (yq ea '. as $i ireduce ({}; . * $i)', jq -s 'reduce ...') require non-obvious incantations for what is a common, simple operation. knf <files> should need no explanation.

Installation

pip install knf-cli

The Python distribution is binary-only: it installs the knf executable and does not provide an importable Python module. Wheels are published for Linux (glibc and musl) on x86-64 and ARM64, macOS on Intel and Apple Silicon, and Windows on x64 and ARM64. No Rust toolchain is needed to install a wheel.

To build from source instead:

cargo install knf-cli

Rust library

The whole pipeline (read paths, parse JSON and TOML, merge, interpolate) is knf-core. It's published separately from the command line, so a Rust consumer or a language binding never pulls in clap:

cargo add knf-core

The library is named knf. Loading, merging and interpolating are three functions, and you compose them:

use knf::{MergeOptions, load_layers, merge};

let (layers, _formats) = load_layers(&["base.toml", "prod.toml"], None)?;
let merged = merge(layers, &MergeOptions::default())?;

load_layers infers each file's format from its extension; pass Some(Format::…) to override (required for -, which reads stdin). It also returns the format each file was read as, so you can pick an output format before merging.

MergeOptions sets strict mode and shallow merge. merge takes any list of knf::Values, so in-memory overlays are just more layers appended after the files. An overlay should be a Value::Object: a scalar layer replaces the whole document instead of shadowing a key. The result is the format-independent knf::Value, ready for a native adapter or language binding to convert without parsing rendered stdout. knf::format::emit renders it when you do want text.

Interpolation is a separate, opt-in step, run once on the merged document. Supply the environment yourself, or use knf::ProcessEnv for the real one:

let merged = knf::interpolate(merged, &knf::ProcessEnv)?;

Errors are typed rather than prose (LoadError, MergeError, InterpError, TomlError) and never name a command-line flag, since a library caller has no command line to act on. A null reaching TOML, for instance, is reported by the paths it was found at. Whether the remedy is spelled -f json is up to your interface, not the library.

Merging

Files are merged left to right in argument order. Exactly one document goes to stdout.

Case Behaviour
object ⊕ object recurse per key
array ⊕ anything replace wholesale, never index-merge or concat
scalar ⊕ anything last wins
anything ⊕ null null is an ordinary value; it overwrites

Two consequences worth knowing:

  • Arrays replace, always. Index-merging would turn ["a"] over ["x","y","z"] into ["a","y","z"] — a value nobody wrote.
  • Null is a value, not a delete. So knf a.json with one argument is always a byte-level no-op.

--strict errors when a layer changes the type of an existing key, which catches the class of mistake where a leaf accidentally shadows a subtree.

$ knf a.json b.json --strict
error: type conflict at `server`: object would be replaced by number

Shallow merge

The default is a deep merge. --shallow merges top-level keys only: a later layer's value replaces the earlier one whole, so keys it omits are dropped. These are jq's two object operators:

knf jq {"db":{"host":"a","port":1}} then {"db":{"host":"b"}}
knf a.json b.json a * b {"db":{"host":"b","port":1}}
knf a.json b.json --shallow a + b {"db":{"host":"b"}}

Arrays replace wholesale in both, exactly as in jq; nothing is ever concatenated. --set layers are ordinary layers, so --shallow --set db.host=x leaves db with nothing but host.

Variable and environment references

A merged config often wants to refer to itself, or to the environment. --interpolate resolves ${key.path} and ${env:VAR} in string values, in one pass over the merged document:

# base.toml
root     = "/srv"
data_dir = "${root}/data"
port     = "${env:PORT}"
url      = "http://localhost:${env:PORT}/health"
literal  = "$${NOT_A_REF}"
$ PORT=8080 knf base.toml --interpolate
root = "/srv"
data_dir = "/srv/data"
port = 8080
url = "http://localhost:8080/health"
literal = "${NOT_A_REF}"

It is opt-in, and off by default. knf sits directly upstream of tools whose own syntax is ${...} — compose files, GitHub Actions workflows, Helm charts, systemd units. Eating those without being asked would be silent corruption, so without the flag the output is byte for byte what it is today.

Where the reference sits decides what it yields:

Position Behaviour
whole string — port = "${p}" takes the referent's value and type; port above is a number, and "${db}" is the whole table
embedded — url = "x/${p}" stringifies; an object or array has no format-independent spelling here, so it is an error

An environment variable is typed by the same rule as --set's right-hand side when it is the whole string, and spliced as raw text when it is embedded — parsing it only to print it again could only lose something.

$$ is a literal $. A $ followed by anything else is ordinary text, so USD $5 needs no escaping.

Document references resolve transitively and in any order; environment values are terminal and are never re-scanned. Cycles are an error, and so is a reference that names nothing:

$ knf base.toml --interpolate
error: unresolved reference
  --> server.url: `db.hostname`
  --> tags[0]: `env:REGION`
help: `${key.path}` names a key in the merged document, `${env:NAME}` an environment variable
help: drop --interpolate to pass `${...}` through untouched

A reference may also read an array element — ${servers[0].host} — with the same two-position rules: whole-string it takes the element's value and type, embedded it stringifies.

Two limits worth knowing:

  • env: is a reserved prefix, matched literally rather than by splitting on the first :. So ${a:b} is the ordinary key a:b, and only keys that literally begin env: are unaddressable.
  • A key spelled with brackets is unaddressable — ${a[0]} now reads as the first element of a, never as a key literally named a[0], and --set 'a[0]=1' is an error rather than a write into an array. Only a file can carry such a key. The same accepted loss as keys containing a literal dot, which the dotted grammars have always excluded.

--set layers interpolate like any other layer. --strict runs during the merge, before any substitution, so it compares the types values had when they were written.

Caveats with formats

JSON and TOML, inferred from the file extension. --input-format overrides it for every input and is required for - (stdin).

Output is the inputs' format when they agree; when they don't, -f is required rather than guessed, so reordering arguments can never silently change the encoding. Pretty-printed by default; --compact opts out.

A TOML datetime is a distinct type all the way through the merge, so every TOML output keeps it unquoted — including a merge that mixed in a JSON layer, and including --set on top. It becomes a plain string only under -f json, where there is nothing else it could be.

TOML cannot represent null, so emitting TOML from a document containing one is an error that names every path:

$ knf base.toml override.json -f toml
error: cannot serialize null to TOML
  --> servers.primary.proxy
  --> logging.sink
help: emit JSON with -f json, substitute with --null-as, or remove the null

Alternatively, you may use --null-as <string> to parse nulls into a custom value:

knf base.toml override.json -f toml --null-as=none

The option is a no-op for JSON output.

Two more values have no spelling in one format or the other, and both are rejected the same way — named by path, never silently substituted.

TOML integers are signed 64-bit, so an ID above i64::MAX (a snowflake, a hash) round-trips exactly through JSON but cannot be written as TOML at all:

$ knf ids.json -f toml
error: cannot serialize integer to TOML
  --> id: `10000000000000000001`
help: TOML integers are signed 64-bit; emit JSON with -f json

Conversely, TOML's number grammar has inf, -inf and nan literals and JSON's has none of them:

$ knf limits.toml -f json
error: cannot serialize non-finite number to JSON
  --> timeout: `inf`
help: emit TOML with -f toml, which can represent inf and nan

Each format is the escape from the other's rejection, and no same-format round-trip is affected: knf ids.json -f json and knf limits.toml -f toml both emit their input unchanged.

Testing

cargo test --workspace
cargo test -p knf-core --lib     # fast inner loop: unit tests only

License

MIT — see LICENSE.

Release files for knf-cli 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for knf-cli 0.3.0
File
knf_cli-0.3.0-py3-none-win_arm64.whl Python 3 none Windows ARM64 Details
knf_cli-0.3.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
knf_cli-0.3.0-py3-none-musllinux_1_2_x86_64.whl Python 3 none Linux musl 1.2+ x86-64 Details
knf_cli-0.3.0-py3-none-musllinux_1_2_aarch64.whl Python 3 none Linux musl 1.2+ ARM64 Details
knf_cli-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details
knf_cli-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl Python 3 none Linux glibc 2.17+ ARM64 Details
knf_cli-0.3.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
knf_cli-0.3.0-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details

Total release size: 5.0 MB

Release files / knf_cli-0.3.0-py3-none-win_arm64.whl

Download URL knf_cli-0.3.0-py3-none-win_arm64.whl
Size 534.9 kB
Tags Python 3 Windows ARM64
SHA-256 checksum
How to use checksums
265aa29271485bf22e814d7ec0bd54b88b8ac482416c963774bf315bbb7eed2a
BLAKE2b-256 checksum
How to use checksums
ee5ee580830541f5d7ed7bb481a97fc71fe31063f6956c70438b46b6a8b675c5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / knf_cli-0.3.0-py3-none-win_amd64.whl

Download URL knf_cli-0.3.0-py3-none-win_amd64.whl
Size 560.8 kB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
8805e57443b508df48b0cef2b56651296eb2e1ba79c4e87a84b3c3701d0c6b82
BLAKE2b-256 checksum
How to use checksums
dc87f582c0ae3046ea66b9a107b119fd4140bd1bd11c07e57c9ae19a41ecac08
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / knf_cli-0.3.0-py3-none-musllinux_1_2_x86_64.whl

Download URL knf_cli-0.3.0-py3-none-musllinux_1_2_x86_64.whl
Size 704.9 kB
Tags Linux musl 1.2+ x86-64 Python 3
SHA-256 checksum
How to use checksums
dddc8f2d9018462950ddd61289c941ca9397f4a3ee8a558d443e860f57668793
BLAKE2b-256 checksum
How to use checksums
4367476dc99cdf58bfc1ddf6df235be7782b04612cc78f20441965693ea87fbc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / knf_cli-0.3.0-py3-none-musllinux_1_2_aarch64.whl

Download URL knf_cli-0.3.0-py3-none-musllinux_1_2_aarch64.whl
Size 659.3 kB
Tags Linux musl 1.2+ ARM64 Python 3
SHA-256 checksum
How to use checksums
34be78f2db900fa8f87066b6958e0a3405fccc7afbab337947add038a7b2dc07
BLAKE2b-256 checksum
How to use checksums
3a29877ede60ec413265ef9107c28764f64fd2ebdf4032b74fbad5ed17f92c17
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / knf_cli-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL knf_cli-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 651.7 kB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
aafdb246a475c846e4d14459bc48fd32dcd60b27f0971775bb32b2017b60f90e
BLAKE2b-256 checksum
How to use checksums
6fbe1e65fe206c87cbee604f0991ff9860c0d0982216089426d033e624b2c95a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / knf_cli-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL knf_cli-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 617.4 kB
Tags Linux glibc 2.17+ ARM64 Python 3
SHA-256 checksum
How to use checksums
9751e9199fb67caeb02356c72e719aa901d86370990173879f8cf514c56b7106
BLAKE2b-256 checksum
How to use checksums
c3db28b41b210edb3da7664401d2e406d993104dc83980277d3b94d1d6a7eba6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / knf_cli-0.3.0-py3-none-macosx_11_0_arm64.whl

Download URL knf_cli-0.3.0-py3-none-macosx_11_0_arm64.whl
Size 606.1 kB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
e2160708a1a13941f544bb55670136e350482090dfa836e6512fc8573e4e08e3
BLAKE2b-256 checksum
How to use checksums
29003b52bd564537fc48199f87ede1b8eb277bef91962ad7adb8a88ab5a75843
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / knf_cli-0.3.0-py3-none-macosx_10_12_x86_64.whl

Download URL knf_cli-0.3.0-py3-none-macosx_10_12_x86_64.whl
Size 626.6 kB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
8dc0a3374e71de5bb26c32af0085ff126b0a90a51a0a0e802b4cacba936c242e
BLAKE2b-256 checksum
How to use checksums
fb2452930c4501b7433276e152e9e0034aa69a4465982b5c22f4727f9df9357b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.3.0 This release

8 release files

0.2.0

8 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page