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 libraries
The whole pipeline — read paths, parse JSON and TOML, merge, interpolate — is
knf-config, published separately from the command line so a Rust consumer or
a language binding never pulls in clap:
cargo add knf-config
use knf::{MergeOpts, merge};
let merged = merge(&["base.toml", "prod.toml"], MergeOpts::default())?;
MergeOpts also accepts strict mode, per-path rules, in-memory terminal
overlays, an input-format override, and opt-in interpolation. An overlay is a
knf::Map rather than a value, for the reason a file layer must be an object at
the top level: a scalar layer would replace 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.
With interpolate set, merge resolves ${env:NAME} against the process
environment; left unset, references are not substituted at all. Pass your own
environment with merge_with_env, and the output is a function of the inputs
alone:
let opts = MergeOpts { interpolate: true, ..MergeOpts::default() };
let merged = knf::merge_with_env(&paths, opts, &my_env)?;
Errors carry typed causes rather than prose — LoadError, MergeError,
InterpError, TomlError — and name no command-line flags, since a library
caller has no command line to act on. A null reaching TOML, for instance, is
reported as the paths it was found at; whether the remedy is spelled -f json
is your interface's business, not the library's.
Map, Value, Rules, Strategy, Format, Env and every error type are
re-exported from knf, along with what they are made of — Number inside
Value::Number, Cycle and Syntax inside InterpError — so a consumer needs
no direct dependency on knf-core or knf-interp to write any of it down.
For merging values that are already in memory, use the smaller core crate — it
has no file I/O and no format crates, only indexmap and thiserror:
cargo add knf-core
use knf_core::{Value, merge};
let merged = merge([base, overlay])?;
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, unless
--appendnames the path. 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.jsonwith 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
Override merge behavior on specific paths
What if a document has one array that should be appended to, and not replaced?
knf understands how to override the merge behavior on a specifc path:
knf base.toml prod.toml --append plugins # concatenate, base ++ prod
knf base.toml prod.toml --replace db # take prod's [db] whole
knf base.toml prod.toml --fail db.host # error if prod overrides db.host
| Flag | At that path |
|---|---|
--append |
concatenate; both sides must be arrays |
--replace |
assign wholesale, no recursion, even object over object |
--fail |
error; the first layer to define the path pins it |
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 keya:b, and only keys that literally beginenv:are unaddressable.- A key spelled with brackets is unaddressable —
${a[0]}now reads as the first element ofa, never as a key literally nameda[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 # fast inner loop: no filesystem, no process
License
MIT — see LICENSE.
Release files for knf-cli 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| knf_cli-0.2.0-py3-none-win_arm64.whl | Python 3 | none | Windows ARM64 | Details |
| knf_cli-0.2.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| knf_cli-0.2.0-py3-none-musllinux_1_2_x86_64.whl | Python 3 | none | Linux musl 1.2+ x86-64 | Details |
| knf_cli-0.2.0-py3-none-musllinux_1_2_aarch64.whl | Python 3 | none | Linux musl 1.2+ ARM64 | Details |
| knf_cli-0.2.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.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| knf_cli-0.2.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| knf_cli-0.2.0-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 5.3 MB
Release files / knf_cli-0.2.0-py3-none-win_arm64.whl
| Download URL | knf_cli-0.2.0-py3-none-win_arm64.whl |
|---|---|
| Size | 570.9 kB |
| Tags | Python 3 Windows ARM64 |
|
SHA-256 checksum How to use checksums |
96cc5cddc7e78b1cd46d3575c171ddef25c408dac8e4b27b593aff8e8933e49b
|
|
BLAKE2b-256 checksum How to use checksums |
01e996f3fbad0981d7ca6630e631616689eb8c7e96bb4030425f32a66afc1f6e
|
| 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.2.0-py3-none-win_amd64.whl
| Download URL | knf_cli-0.2.0-py3-none-win_amd64.whl |
|---|---|
| Size | 606.2 kB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
80e5d41fb55a877696e81ac6189a9b189dbe2f577a89c56165f2ed1f52ecba34
|
|
BLAKE2b-256 checksum How to use checksums |
d295e9ed33b7e8837622ff64ddd8c2faad7be7e54c4812d076872ee3fd33a9d3
|
| 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.2.0-py3-none-musllinux_1_2_x86_64.whl
| Download URL | knf_cli-0.2.0-py3-none-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 751.8 kB |
| Tags | Linux musl 1.2+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
39c3aa437474916ffa4347b4e30077bacff7c373696fef669cbe75f329de3d67
|
|
BLAKE2b-256 checksum How to use checksums |
3d7cf5847b68ee9e055dd6460f072da94838bdf819200d2bee182441bef704b6
|
| 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.2.0-py3-none-musllinux_1_2_aarch64.whl
| Download URL | knf_cli-0.2.0-py3-none-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 694.8 kB |
| Tags | Linux musl 1.2+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
141c691a78ab2c64cea75f4436bbddf73358e274814d12bc75fab6e159e887dd
|
|
BLAKE2b-256 checksum How to use checksums |
927400afd3c3cd0e23cf9d44f8d749b445db90d177972f1f50f38d2ea466543c
|
| 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.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | knf_cli-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 699.6 kB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
1ff50040a06b111249c82e803f71b9f522a6653d3a0fefd31144cef1a6f9372d
|
|
BLAKE2b-256 checksum How to use checksums |
31aedebda60b970e476c2f916fb7e3db9d2ae6e548033ed0b0ae76cf4cfc2486
|
| 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.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | knf_cli-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 653.9 kB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
2f4dae1ab53b7fa321981f572f78a37db8b91c7ec26bb287ae6bfdadd9e95e7e
|
|
BLAKE2b-256 checksum How to use checksums |
da09460e58223200b530cac9bb89026189550e1565c0aa626dc717f31d17d708
|
| 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.2.0-py3-none-macosx_11_0_arm64.whl
| Download URL | knf_cli-0.2.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 646.1 kB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
fa2f7e1b75f02a2832b17d8fd76dd39502c977bac96897fb0dc9cd7b51397611
|
|
BLAKE2b-256 checksum How to use checksums |
6c114a4a5d710ecbcdd0eb1ed623390366de06503b6d8996ea3dbc77b901c793
|
| 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.2.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | knf_cli-0.2.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 669.3 kB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
e09931ca626751adcf7cb6b97db68d741658205a40e722908912ac0281aba21f
|
|
BLAKE2b-256 checksum How to use checksums |
0c06fa8a76254e938802dc497ad1f60ca93c97ca3db978d90ea67d56e148fb7c
|
| 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}
|