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 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 --append names 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.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

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 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           # 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)

Table of built distributions (wheels) for knf-cli 0.2.0
File
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}

Release history Release notifications | RSS feed

0.3.0

8 release files

This release

0.2.0 This release

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