#!/usr/bin/node /* The MIT License (MIT) Copyright © 2020-2025 pacman64 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // #!/usr/bin/node --trace-uncaught 'use strict'; const info = ` tjn [options...] [node-js expression] [filepath/URI...] Transform Json with Node loads JSON data, runs a NodeJS expression on it, and emits the result as JSON. Parsed input-data are available to the given expression as any of the variables named 'v', 'value', 'd', and 'data'. If no file/URI is given, it loads JSON data from its standard input. If the argument before the expression is a single equals sign (a '=', without the quotes), no data are read/parsed, and the expression is evaluated as given. Options, where leading double-dashes are also allowed, except for alias '=': -c compact single-line JSON output (JSON-0) -compact same as -c -j0 same as -c -json0 same as -c -h show this help message -help same as -h -nil don't read any input -none same as -nil -null same as -nil = same as -nil `; if (process.argv.length < 3) { process.stdout.write(info.trim()); process.stdout.write('\n'); process.exit(0); } if (process.argv.length === 3) { switch (process.argv[2]) { case '-h': case '--h': case '-help': case '--help': process.stdout.write(info.trim()); process.stdout.write('\n'); process.exit(0); break; } } const readFileSync = require('fs').readFileSync; require = function () { throw `function 'require' is disabled`; }; function main() { let srcIndex = 2; let useInput = true; let json0 = false; out: while (srcIndex < process.argv.length) { switch (process.argv[srcIndex]) { case '=': case '-nil': case '--nil': case '-none': case '--none': case '-null': case '--null': useInput = false; srcIndex++; break; case '-c': case '--c': case '-compact': case '--compact': case '-j0': case '--j0': case '-json0': case '--json0': json0 = true; srcIndex++; break; default: break out; } } try { if (useInput && process.argv.length > srcIndex + 2) { throw `multiple named inputs not supported`; } const args = process.argv.slice(srcIndex + 1); const expr = process.argv[srcIndex]; const show = json0 ? showJSON0 : showJSON; globalThis.now = new Date(); globalThis.args = args; // ignore broken-pipe-style errors, making piping output to the // likes of `head` just work as intended process.stdout.on('error', _ => { process.exit(0); }); if (!useInput) { show(run(null, expr)); return; } if (process.argv.length === srcIndex + 2) { const name = process.argv[process.argv.length - 1]; if (name.startsWith('https://') || name.startsWith('http://')) { fetch(name).then(resp => resp.json()).then(data => { show(run(data, expr)); }); return; } if (name === '-') { show(run(JSON.parse(readFileSync(0)), expr)); return; } let path = name; if (name.startsWith('file://')) { path = name.slice('file://'.length); } show(run(JSON.parse(readFileSync(path)), expr)); return; } show(run(JSON.parse(readFileSync(0)), expr)); } catch (error) { process.stderr.write(`\x1b[31m${error}\x1b[0m\n`); process.exit(1); } } function conform(x) { if (x == null) { return x; } if (Array.isArray(x)) { return x.filter(e => !(e instanceof Skip)).map(e => conform(e)); } switch (typeof x) { case 'bigint': return x.toString(); case 'boolean': case 'number': case 'string': return x; case 'object': const kv = {}; for (const k in x) { if (Object.prototype.hasOwnProperty.call(x, k)) { const v = kv[k]; if (!(v instanceof Skip)) { kv[k] = conform(v); } } } return kv; default: return x.toString(); } } function conforms(x) { if (x == null) { return true; } if (x instanceof Skip) { return false; } if (Array.isArray(x)) { for (const e of x) { if (!conforms(e)) { return false; } } return true; } switch (typeof x) { case 'boolean': case 'number': case 'string': return true; case 'object': for (const k in x) { if (Object.prototype.hasOwnProperty.call(x, k)) { if (!conforms(x[k])) { return false; } } } return true; default: return false; } } function showJSON(data) { process.stdout.write(JSON.stringify(data, null, 2)); process.stdout.write('\n'); } function showJSON0(data) { process.stdout.write(JSON.stringify(data, null, 0)); process.stdout.write('\n'); } function run(data, expr) { switch (expr.trim()) { case '', '.': return data; } const d = data; const v = data; const value = data; let res = eval(expr); if (typeof res === 'function') { res = res(data); } return conforms(res) ? res : conform(res); } class Skip { } const skip = new Skip(); const bquo = '`'; const bquote = '`'; const cr = '\r'; const crlf = '\r\n'; const dquo = '"'; const dquote = '"'; const lcurly = '{'; const rcurly = '}'; const squo = '\''; const squote = '\''; function chunk(what, chunkSize) { const chunks = []; for (let i = 0; i < what.length; i += chunkSize) { const end = Math.min(i + chunkSize, what.length); chunks.push(what.slice(i, end)); } return chunks; } function cond(...args) { for (let i = 0; i < args.length - args.length % 2; i += 2) { if (args[i]) { return args[i + 1]; } } return len(args) % 2 === 0 ? null : args[args.length - 1]; } function dive(into, using) { if (using == null) { throw 'dive: no diving func given'; } function go(into, using, key, src) { if (Array.isArray(into)) { return into.map((x, i) => go(x, using, i, into)); } if (typeof into === 'object') { const kv = {}; for (const k in into) { if (Object.hasOwnProperty.call(into, k)) { kv[k] = go(into[k], using, k, into); } } return kv; } return using(into, key, src); } if (typeof into === 'function') { return go(using, into, null, into); } return go(into, using, null, into); } function divebin(x, y, using) { if (using == null) { throw 'divebin: no diving func given'; } if (typeof x === 'function') { return divebin(y, using, x) } switch (using.length) { case 2: return dive(x, a => dive(y, b => using(a, b))); case 4: return dive(x, (a, i) => dive(y, (b, j) => using(a, i, b, j))); default: throw `divebin(...) only supports funcs with 2 or 4 args`; } } const bindive = divebin; function ints(start, stop, f = identity) { return iota(stop - start + 1, n => f(n + start)); } function iota(n, f = identity) { if (n < 1 || isNaN(n) || !isFinite(n)) { return []; } n = Math.floor(n); const range = new Array(n); for (let i = 0; i < n; i++) { range[i] = f(i + 1); } return range; } function join(what, sep = '') { if (Array.isArray(what) && Array.isArray(sep)) { const kv = {}; for (let i = 0; i < what.length; i++) { kv[what[i]] = i < sep.length ? sep[i] : null; } return kv; } if (typeof what === 'string') { return sep.join(what); } return what.join(sep); } function rescue(attempt, fallback) { try { return attempt() } catch (e) { if (typeof fallback !== 'function') { return fallback; } return fallback.length === 0 ? fallback() : fallback(e); } } const recover = rescue; main();