kv-source
kv-source reads plain KEY=VALUE text (a FILE arg, or stdin) and emits one uniform JSON object per assignment — {"key":NAME,"value":VALUE}, both strings — the front of a pipe you feed into the fold/filter/transform gifts (kv-source .env | dedup-filter). Lines split on the FIRST '=', full-line # comments and blank lines skipped, outer quotes stripped, CRLF==LF. Zero dependencies, runs unchanged in Node or a browser, same input yields byte-identical output every run.
The honest edge
kv-source parses PLAIN KEY=VALUE text into uniform string records only — it does not coerce types ("8080" stays a string, never a number), does not interpolate ${VAR}, does not strip an `export ` prefix, does not honor inline comments, and does not process escapes inside quotes. Each of those is a context-dependent guess that would make the same file parse to different bytes across tools. A malformed line (no '='), an illegal/empty key, or --key-field==--value-field is refused (exit 2). An honest, pinnable parse — not a full dotenv runtime.
Run it
printf 'PORT=8080\n' | node kv-source.js # -> {"key":"PORT","value":"8080"}
test_kv-source.js (50/50, independent regex-route oracle) + Plumb conformance GREEN (28/28, signed 2026-09-12, clock-independent, mutation-bite non-vacuous)
Node / browser, no dependencies
The code — every file that ships
kv-source.js239 lineson GitHub →
#!/usr/bin/env node
/* kv-source.js — parse plain KEY=VALUE text into a uniform JSONL record stream.
Dependency-free, deterministic, pure. Runs in Node or a browser. MIT.
WHAT IT IS. A SOURCE: it turns non-JSONL text (a .env / .properties / a plain
key=value config) into the front of a pipe — one JSON object per line (JSONL) that
the fold/filter/transform gifts consume. Give it a file (or pipe text on stdin) and
it emits one uniform record per assignment:
PORT=8080 -> {"key":"PORT","value":"8080"}
# a comment -> (skipped)
HOST = localhost -> {"key":"HOST","value":"localhost"}
GREETING="hi there"-> {"key":"GREETING","value":"hi there"}
Every record has the same two string fields, so it drops straight into a generic
filter or fold: kv-source .env | range-filter ... / kv-source .env | dedup-filter.
STRINGS ONLY (the honesty axis). Values are emitted as LITERAL STRINGS — kv-source
does not guess types. "8080" stays the string "8080"; "true" stays "true"; "" stays
"". This is deliberate, not a limitation to apologize for: the moment a parser starts
coercing (is 08 octal? is 1e3 a number? is TRUE a boolean?) the SAME file parses to
different bytes under different tools, and a source you cannot pin is not a source.
kv-source emits strings and lets a downstream typed gift decide. For the same reason
it does NOT interpolate ${VAR}/$VAR, does NOT strip an `export ` prefix, does NOT
honor inline (trailing) comments, and does NOT process backslash escapes inside a
quoted value — each of those is a context-dependent guess that would break byte
determinism. It parses the grammar it was given and REFUSES the rest, loudly.
THE GRAMMAR (declared, so it is pinnable)
* Lines split on newline; a trailing CR (CRLF files) is stripped, so a file parses
identically whether it uses LF or CRLF.
* A whitespace-only line emits nothing.
* A line whose first non-whitespace character is `#` is a full-line COMMENT and
emits nothing. (There are no inline/trailing comments — a `#` inside a value is
part of the value.)
* Any other line MUST be KEY=VALUE. It is split on the FIRST `=`; everything before
is the key, everything after is the value.
- KEY is trimmed and MUST match [A-Za-z_][A-Za-z0-9_.-]* — a non-empty name.
An empty key, or a key with spaces/illegal characters (e.g. `export FOO`), is
a hard error (exit 2), not a silent guess.
- VALUE is trimmed. If, after trimming, it both begins and ends with a matching
`"` or `'` (length >= 2), those OUTER quotes are stripped and the inner text
is the literal value (no escape processing). Otherwise the trimmed remainder
is the literal value.
* A non-blank, non-comment line with no `=` is a MALFORMED line (exit 2).
THE MODEL
[FILE] The key=value file to parse. If omitted, read stdin.
--key-field NAME The object key the parsed KEY is emitted under. Default "key".
--value-field NAME The object key the parsed VALUE is emitted under. Default
"value". Must differ from --key-field (else the record would
collapse) — a collision is exit 2.
Duplicate keys across lines are NOT merged or de-duplicated — each line emits its own
record, in file order. De-duplication (last-wins, first-wins) is dedup-filter's job
downstream; kv-source's job is an honest, order-faithful parse.
DETERMINISM. parse(text, opts) is a pure function — no clock, no randomness, no
network — so the same text yields byte-identical output every run and every machine.
USAGE
node kv-source.js config.env
cat .env | node kv-source.js
node kv-source.js .env --key-field name --value-field val
node kv-source.js --help
Exit codes: 0 success (including an empty stream from empty input) · 2 input error
(malformed line, illegal/empty key, key-field==value-field, empty field name, unknown
option, unreadable file). Always a clean one-line message on stderr, never a stack
trace.
Released under MIT. Its edge is printed in the README: kv-source parses PLAIN
KEY=VALUE text into uniform string records only — it does not coerce types, does not
interpolate ${VAR}, does not strip an `export ` prefix, does not honor inline
comments, and does not process escapes inside quotes. An honest, pinnable parse.
*/
"use strict";
/* ---- the pure core ------------------------------------------------ */
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_.\-]*$/;
// Strip one pair of matching outer quotes from an already-trimmed value, if present.
function stripQuotes(v) {
if (v.length >= 2) {
var a = v.charAt(0), b = v.charAt(v.length - 1);
if ((a === '"' && b === '"') || (a === "'" && b === "'")) {
return v.slice(1, v.length - 1);
}
}
return v;
}
// Parse KEY=VALUE text into an array of uniform two-field records. Throws a clean Error
// on any malformed input — the CLI turns that into exit 2. Pure; no side effects.
function parse(text, opts) {
opts = opts || {};
var keyField = opts.keyField === undefined ? "key" : opts.keyField;
var valueField = opts.valueField === undefined ? "value" : opts.valueField;
if (typeof keyField !== "string" || keyField.length === 0) throw new Error("--key-field must be a non-empty name");
if (typeof valueField !== "string" || valueField.length === 0) throw new Error("--value-field must be a non-empty name");
if (keyField === valueField) throw new Error("--key-field and --value-field must differ (both \"" + keyField + "\")");
if (typeof text !== "string") throw new Error("input must be text");
var out = [];
var lines = text.split("\n");
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
// strip a trailing CR so CRLF parses identically to LF
if (line.charAt(line.length - 1) === "\r") line = line.slice(0, line.length - 1);
var trimmed = line.replace(/^\s+/, "").replace(/\s+$/, "");
if (trimmed.length === 0) continue; // blank line
if (trimmed.charAt(0) === "#") continue; // full-line comment
var eq = line.indexOf("=");
if (eq === -1) throw new Error("malformed line " + (i + 1) + " (no '='): " + JSON.stringify(line));
var key = line.slice(0, eq).replace(/^\s+/, "").replace(/\s+$/, "");
var value = line.slice(eq + 1).replace(/^\s+/, "").replace(/\s+$/, "");
if (!KEY_RE.test(key)) throw new Error("illegal key on line " + (i + 1) + ": " + JSON.stringify(key) + " (keys must match [A-Za-z_][A-Za-z0-9_.-]*)");
value = stripQuotes(value);
var rec = {};
rec[keyField] = key;
rec[valueField] = value;
out.push(rec);
}
return out;
}
// Render records as JSONL text (one JSON object per line, trailing newline if any).
function toJSONL(records) {
var s = "";
for (var i = 0; i < records.length; i++) s += JSON.stringify(records[i]) + "\n";
return s;
}
/* ---- exports (browser + Node) ------------------------------------ */
if (typeof window !== "undefined") {
window.ForestGifts = window.ForestGifts || {};
window.ForestGifts.kvSource = { parse: parse, toJSONL: toJSONL };
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { parse: parse, toJSONL: toJSONL };
}
/* ---- CLI (runs only when invoked directly, never on require) ------ */
function parseArgs(args) {
var opts = {};
var file;
var i = 0;
while (i < args.length) {
var a = args[i];
if (a === "--key-field") {
if (args[i + 1] === undefined) throw new Error("--key-field requires a name");
opts.keyField = args[i + 1];
i += 2;
} else if (a === "--value-field") {
if (args[i + 1] === undefined) throw new Error("--value-field requires a name");
opts.valueField = args[i + 1];
i += 2;
} else if (a.charAt(0) === "-" && a !== "-") {
throw new Error("unknown option " + a);
} else {
if (file !== undefined) throw new Error("only one input file may be given (got a second: " + JSON.stringify(a) + ")");
file = a;
i += 1;
}
}
return { file: file, opts: opts };
}
function readAll(stream) {
return new Promise(function (resolve, reject) {
var chunks = [];
stream.on("data", function (c) { chunks.push(c); });
stream.on("end", function () { resolve(Buffer.concat(chunks).toString("utf8")); });
stream.on("error", reject);
});
}
function helpText() {
return (
"kv-source.js — parse plain KEY=VALUE text into a uniform JSONL record stream.\n\n" +
" node kv-source.js config.env\n" +
" cat .env | node kv-source.js\n" +
" node kv-source.js .env --key-field name --value-field val\n" +
" node kv-source.js --help\n\n" +
" [FILE] key=value file to parse (default: read stdin)\n" +
" --key-field NAME field the parsed KEY is emitted under (default \"key\")\n" +
" --value-field NAME field the parsed VALUE is emitted under (default \"value\")\n\n" +
"Emits one JSON object per assignment: { key: NAME, value: VALUE }, one per line.\n" +
"Full-line # comments and blank lines are skipped; lines split on the first '='.\n\n" +
"Edge: PLAIN KEY=VALUE only, values are literal STRINGS. It does not coerce types,\n" +
"does not interpolate ${VAR}, does not strip an `export ` prefix, does not honor\n" +
"inline comments, and does not process escapes inside quotes. An honest, pinnable parse.\n"
);
}
function main(argv) {
var args = argv.slice(2);
if (args.indexOf("--help") !== -1 || args.indexOf("-h") !== -1) {
process.stdout.write(helpText());
return Promise.resolve(0);
}
var parsed;
try { parsed = parseArgs(args); }
catch (e) { process.stderr.write("kv-source: " + e.message + "\n"); return Promise.resolve(2); }
var getText;
if (parsed.file !== undefined) {
getText = new Promise(function (resolve, reject) {
require("fs").readFile(parsed.file, "utf8", function (err, data) {
if (err) reject(new Error("cannot read " + JSON.stringify(parsed.file) + ": " + err.code));
else resolve(data);
});
});
} else {
getText = readAll(process.stdin);
}
return getText.then(function (text) {
var records = parse(text, parsed.opts); // throws -> caught below
process.stdout.write(toJSONL(records));
return 0;
}).catch(function (e) {
process.stderr.write("kv-source: " + e.message + "\n");
return 2;
});
}
if (typeof require !== "undefined" && require.main === module) {
main(process.argv).then(function (code) { process.exitCode = code; });
}
test_kv-source.js147 lineson GitHub →
#!/usr/bin/env node
/* test_kv-source.js — battery for the kv-source gift.
node test_kv-source.js -> exit 0 PASS / non-zero FAIL. Zero dependencies.
Cross-checks the gift against an INDEPENDENT regex-route oracle (a different parse
route than the gift's indexOf-split), frozen hand goldens, the uniform-record and
strings-only contract, determinism, and every fail-closed edge. */
"use strict";
var kv = require("./kv-source.js");
var pass = 0, fail = 0;
function ok(name, cond) { if (cond) { pass++; } else { fail++; console.log(" FAIL " + name); } }
function eq(name, a, b) {
var same = JSON.stringify(a) === JSON.stringify(b);
if (!same) console.log(" FAIL " + name + "\n exp " + JSON.stringify(b) + "\n got " + JSON.stringify(a));
else pass++;
if (!same) fail++;
}
function throws(name, fn) {
var threw = false;
try { fn(); } catch (e) { threw = true; }
if (threw) pass++; else { fail++; console.log(" FAIL " + name + " (expected a throw)"); }
}
/* ---- independent oracle: regex-route (different route than the gift) ---- */
function oracle(text, opts) {
opts = opts || {};
var kf = opts.keyField === undefined ? "key" : opts.keyField;
var vf = opts.valueField === undefined ? "value" : opts.valueField;
if (!kf || !vf || kf === vf) throw new Error("bad fields");
var out = [];
var lines = text.split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
var ln = lines[i];
if (/^\s*$/.test(ln)) continue;
if (/^\s*#/.test(ln)) continue;
var m = ln.match(/^\s*([A-Za-z_][A-Za-z0-9_.\-]*)\s*=([\s\S]*)$/);
if (!m) throw new Error("bad line " + (i + 1));
var val = m[2].replace(/^\s+/, "").replace(/\s+$/, "");
if (val.length >= 2) {
var a = val.charAt(0), b = val.charAt(val.length - 1);
if ((a === '"' && b === '"') || (a === "'" && b === "'")) val = val.slice(1, val.length - 1);
}
var r = {}; r[kf] = m[1]; r[vf] = val; out.push(r);
}
return out;
}
/* ---- 1. differential grid: gift == oracle on every good vector ---- */
var goodVectors = [
"PORT=8080",
"A=1\nB=2\nC=3",
"# a comment\nHOST=localhost\n\n# another\nPORT=80",
" SPACED = value here ", // trim both sides
'GREETING="hi there"', // double-quoted, embedded space
"MSG='single quoted'",
"URL=http://example.com/path?a=b&c=d", // first-= split; value keeps later '='
"EMPTY=", // empty value -> ""
"HASHVAL=a#b#c", // '#' inside a value is literal (no inline comments)
"K.dotted=1\nk-dashed=2\n_under=3", // legal key charset
"Q=\"with = and # inside\"", // quotes protect nothing special but are stripped
"CRLF=one\r\nSECOND=two\r\n", // CRLF parses same as LF
"", // empty input -> empty stream
"\n\n \n# only comments and blanks\n", // -> empty stream
"EQ====", // key EQ, value "===" (first '=' splits)
];
for (var gi = 0; gi < goodVectors.length; gi++) {
var t = goodVectors[gi];
eq("differential[" + gi + "] gift==oracle", kv.parse(t), oracle(t));
}
/* ---- 2. hand goldens (frozen) ---- */
eq("golden: basic", kv.parse("PORT=8080"), [{ key: "PORT", value: "8080" }]);
eq("golden: three", kv.parse("A=1\nB=2\nC=3"),
[{ key: "A", value: "1" }, { key: "B", value: "2" }, { key: "C", value: "3" }]);
eq("golden: comment+blank skipped", kv.parse("# c\n\nHOST=localhost"), [{ key: "HOST", value: "localhost" }]);
eq("golden: trim both sides", kv.parse(" K = v "), [{ key: "K", value: "v" }]);
eq("golden: double-quote stripped", kv.parse('G="hi there"'), [{ key: "G", value: "hi there" }]);
eq("golden: single-quote stripped", kv.parse("G='hi there'"), [{ key: "G", value: "hi there" }]);
eq("golden: first-= split keeps rest", kv.parse("U=a=b=c"), [{ key: "U", value: "a=b=c" }]);
eq("golden: empty value", kv.parse("E="), [{ key: "E", value: "" }]);
eq("golden: hash in value literal", kv.parse("H=a#b"), [{ key: "H", value: "a#b" }]);
eq("golden: CRLF == LF", kv.parse("A=1\r\nB=2\r\n"), kv.parse("A=1\nB=2\n"));
eq("golden: empty input empty stream", kv.parse(""), []);
eq("golden: only comments/blanks empty", kv.parse("\n#x\n \n"), []);
/* ---- 3. uniform 2-field, strings-only ---- */
var recs = kv.parse("N=8080\nB=true\nF=1.5\nZ=");
ok("uniform: every record has exactly 2 keys", recs.every(function (r) { return Object.keys(r).length === 2; }));
ok("uniform: fields are key+value", recs.every(function (r) { return "key" in r && "value" in r; }));
ok("strings-only: numbers stay strings", recs[0].value === "8080" && typeof recs[0].value === "string");
ok("strings-only: bool stays string", recs[1].value === "true" && typeof recs[1].value === "string");
ok("strings-only: float stays string (no coercion/drift)", recs[2].value === "1.5" && typeof recs[2].value === "string");
/* ---- 4. custom field names ---- */
eq("fields: custom names", kv.parse("A=1", { keyField: "name", valueField: "val" }), [{ name: "A", val: "1" }]);
eq("fields: order key then value", Object.keys(kv.parse("A=1", { keyField: "k2", valueField: "v2" })[0]), ["k2", "v2"]);
/* ---- 5. no interpolation / no export / no inline comment ---- */
eq("no interpolation: ${VAR} literal", kv.parse("A=${HOME}/bin"), [{ key: "A", value: "${HOME}/bin" }]);
eq("no inline comment: trailing # kept", kv.parse("A=v # not a comment"), [{ key: "A", value: "v # not a comment" }]);
throws("no export: `export FOO` is an illegal key", function () { kv.parse("export FOO=bar"); });
/* ---- 6. fail-closed (exit-2 class) ---- */
throws("bad: line with no '='", function () { kv.parse("JUST_A_LINE"); });
throws("bad: empty key", function () { kv.parse("=value"); });
throws("bad: key with space", function () { kv.parse("bad key=1"); });
throws("bad: key starts with digit", function () { kv.parse("9key=1"); });
throws("bad: key with illegal char", function () { kv.parse("k!=1"); });
throws("bad: key-field == value-field", function () { kv.parse("A=1", { keyField: "x", valueField: "x" }); });
throws("bad: empty key-field", function () { kv.parse("A=1", { keyField: "" }); });
throws("bad: empty value-field", function () { kv.parse("A=1", { valueField: "" }); });
throws("bad: non-string input", function () { kv.parse(123); });
/* ---- 7. determinism: parse twice -> byte-identical JSONL ---- */
var big = "";
for (var k = 0; k < 50; k++) big += "K" + k + "=v" + k + "\n";
ok("determinism: two parses byte-identical", kv.toJSONL(kv.parse(big)) === kv.toJSONL(kv.parse(big)));
/* ---- 8. toJSONL shape ---- */
ok("toJSONL: one object per line + trailing nl", kv.toJSONL([{ key: "A", value: "1" }]) === '{"key":"A","value":"1"}\n');
ok("toJSONL: empty records -> empty string", kv.toJSONL([]) === "");
/* ---- 9. mutation tripwire: a LAST-'=' split must diverge on a value containing '=' ---- */
(function () {
function mutantLastEq(text) {
var out = [], lines = text.split("\n");
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.charAt(line.length - 1) === "\r") line = line.slice(0, -1);
var tr = line.replace(/^\s+|\s+$/g, "");
if (!tr || tr.charAt(0) === "#") continue;
var eqp = line.lastIndexOf("="); // BUG: last instead of first
if (eqp === -1) throw new Error("no =");
var key = line.slice(0, eqp).replace(/^\s+|\s+$/g, "");
var val = line.slice(eqp + 1).replace(/^\s+|\s+$/g, "");
var r = {}; r.key = key; r.value = val; out.push(r);
}
return out;
}
var v = "U=a=b";
ok("mutation: last-= split is CAUGHT (diverges from gift)",
JSON.stringify(mutantLastEq(v)) !== JSON.stringify(kv.parse(v)));
})();
console.log((fail === 0 ? "PASS" : "FAIL") + " " + pass + "/" + (pass + fail));
process.exit(fail === 0 ? 0 : 1);