File: tjn.js
   1 #!/usr/bin/node
   2 
   3 /*
   4 The MIT License (MIT)
   5 
   6 Copyright © 2020-2025 pacman64
   7 
   8 Permission is hereby granted, free of charge, to any person obtaining a copy of
   9 this software and associated documentation files (the “Software”), to deal
  10 in the Software without restriction, including without limitation the rights to
  11 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  12 of the Software, and to permit persons to whom the Software is furnished to do
  13 so, subject to the following conditions:
  14 
  15 The above copyright notice and this permission notice shall be included in all
  16 copies or substantial portions of the Software.
  17 
  18 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  21 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  22 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  23 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  24 SOFTWARE.
  25 */
  26 
  27 // #!/usr/bin/node --trace-uncaught
  28 
  29 'use strict';
  30 
  31 const info = `
  32 tjn [options...] [node-js expression] [filepath/URI...]
  33 
  34 
  35 Transform Json with Node loads JSON data, runs a NodeJS expression on it,
  36 and emits the result as JSON. Parsed input-data are available to the given
  37 expression as any of the variables named 'v', 'value', 'd', and 'data'.
  38 
  39 If no file/URI is given, it loads JSON data from its standard input. If the
  40 argument before the expression is a single equals sign (a '=', without the
  41 quotes), no data are read/parsed, and the expression is evaluated as given.
  42 
  43 Options, where leading double-dashes are also allowed, except for alias '=':
  44 
  45     -c          compact single-line JSON output (JSON-0)
  46     -compact    same as -c
  47     -j0         same as -c
  48     -json0      same as -c
  49 
  50     -h          show this help message
  51     -help       same as -h
  52 
  53     -nil         don't read any input
  54     -none        same as -nil
  55     -null        same as -nil
  56     =            same as -nil
  57 `;
  58 
  59 if (process.argv.length < 3) {
  60     process.stdout.write(info.trim());
  61     process.stdout.write('\n');
  62     process.exit(0);
  63 }
  64 
  65 if (process.argv.length === 3) {
  66     switch (process.argv[2]) {
  67     case '-h':
  68     case '--h':
  69     case '-help':
  70     case '--help':
  71         process.stdout.write(info.trim());
  72         process.stdout.write('\n');
  73         process.exit(0);
  74         break;
  75     }
  76 }
  77 
  78 const readFileSync = require('fs').readFileSync;
  79 
  80 require = function () {
  81     throw `function 'require' is disabled`;
  82 };
  83 
  84 
  85 function main() {
  86     let srcIndex = 2;
  87     let useInput = true;
  88     let json0 = false;
  89 
  90     out:
  91     while (srcIndex < process.argv.length) {
  92         switch (process.argv[srcIndex]) {
  93         case '=':
  94         case '-nil':
  95         case '--nil':
  96         case '-none':
  97         case '--none':
  98         case '-null':
  99         case '--null':
 100             useInput = false;
 101             srcIndex++;
 102             break;
 103 
 104         case '-c':
 105         case '--c':
 106         case '-compact':
 107         case '--compact':
 108         case '-j0':
 109         case '--j0':
 110         case '-json0':
 111         case '--json0':
 112             json0 = true;
 113             srcIndex++;
 114             break;
 115 
 116         default:
 117             break out;
 118         }
 119     }
 120 
 121     function showJSON(data, compact) {
 122         process.stdout.write(JSON.stringify(data, null, compact ? 0 : 2));
 123         process.stdout.write('\n');
 124     }
 125 
 126     try {
 127         if (useInput && process.argv.length > srcIndex + 2) {
 128             throw `multiple named inputs not supported`;
 129         }
 130 
 131         const args = process.argv.slice(srcIndex + 1);
 132         const expr = process.argv[srcIndex];
 133         globalThis.now = new Date();
 134         globalThis.args = args;
 135 
 136         // ignore broken-pipe-style errors, making piping output to the
 137         // likes of `head` just work as intended
 138         process.stdout.on('error', _ => { process.exit(0); });
 139 
 140         if (!useInput) {
 141             showJSON(run(null, expr), json0);
 142             return;
 143         }
 144 
 145         if (process.argv.length === srcIndex + 2) {
 146             const name = process.argv[process.argv.length - 1];
 147 
 148             if (name.startsWith('https://') || name.startsWith('http://')) {
 149                 fetch(name).then(resp => resp.json()).then(data => {
 150                     showJSON(run(data, expr), compact);
 151                 });
 152                 return;
 153             }
 154 
 155             if (name === '-') {
 156                 showJSON(run(JSON.parse(readFileSync(0)), expr), json0);
 157                 return;
 158             }
 159 
 160             let path = name;
 161             if (name.startsWith('file://')) {
 162                 path = name.slice('file://'.length);
 163             }
 164             showJSON(run(JSON.parse(readFileSync(path)), expr), json0);
 165             return;
 166         }
 167 
 168         showJSON(run(JSON.parse(readFileSync(0)), expr), json0);
 169     } catch (error) {
 170         process.stderr.write(`\x1b[31m${error}\x1b[0m\n`);
 171         process.exit(1);
 172     }
 173 }
 174 
 175 function run(data, expr) {
 176     switch (expr.trim()) {
 177     case '', '.':
 178         return data;
 179     }
 180 
 181     const d = data;
 182     const v = data;
 183     const value = data;
 184 
 185     let res = eval(expr);
 186     if (typeof res === 'function') {
 187         res = res(data);
 188     }
 189     return conforms(res) ? res : conform(res);
 190 }
 191 
 192 function conform(x) {
 193     if (x == null) {
 194         return x;
 195     }
 196 
 197     if (Array.isArray(x)) {
 198         return x.filter(e => !(e instanceof Skip)).map(e => conform(e));
 199     }
 200 
 201     switch (typeof x) {
 202     case 'bigint':
 203         return x.toString();
 204 
 205     case 'boolean':
 206     case 'number':
 207     case 'string':
 208         return x;
 209 
 210     case 'object':
 211         const kv = {};
 212         for (const k in x) {
 213             if (Object.prototype.hasOwnProperty.call(x, k)) {
 214                 const v = kv[k];
 215                 if (!(v instanceof Skip)) {
 216                     kv[k] = conform(v);
 217                 }
 218             }
 219         }
 220         return kv;
 221 
 222     default:
 223         return x.toString();
 224     }
 225 }
 226 
 227 function conforms(x) {
 228     if (x == null) {
 229         return true;
 230     }
 231 
 232     if (x instanceof Skip) {
 233         return false;
 234     }
 235 
 236     if (Array.isArray(x)) {
 237         for (const e of x) {
 238             if (!conforms(e)) {
 239                 return false;
 240             }
 241         }
 242         return true;
 243     }
 244 
 245     switch (typeof x) {
 246     case 'boolean':
 247     case 'number':
 248     case 'string':
 249         return true;
 250 
 251     case 'object':
 252         for (const k in x) {
 253             if (Object.prototype.hasOwnProperty.call(x, k)) {
 254                 if (!conforms(x[k])) {
 255                     return false;
 256                 }
 257             }
 258         }
 259         return true;
 260 
 261     default:
 262         return false;
 263     }
 264 }
 265 
 266 
 267 class Skip { }
 268 
 269 const skip = new Skip();
 270 
 271 const bquo = '`';
 272 const bquote = '`';
 273 const cr = '\r';
 274 const crlf = '\r\n';
 275 const dquo = '"';
 276 const dquote = '"';
 277 const lcurly = '{';
 278 const rcurly = '}';
 279 const squo = '\'';
 280 const squote = '\'';
 281 
 282 const nil = null;
 283 const none = null;
 284 
 285 
 286 function chunk(what, chunkSize) {
 287     const chunks = [];
 288     for (let i = 0; i < what.length; i += chunkSize) {
 289         const end = Math.min(i + chunkSize, what.length);
 290         chunks.push(what.slice(i, end));
 291     }
 292     return chunks;
 293 }
 294 
 295 function cond(...args) {
 296     for (let i = 0; i < args.length - args.length % 2; i += 2) {
 297         if (args[i]) {
 298             return args[i + 1];
 299         }
 300     }
 301     return len(args) % 2 === 0 ? null : args[args.length - 1];
 302 }
 303 
 304 function dive(into, using) {
 305     if (using == null) {
 306         throw 'dive: no diving func given';
 307     }
 308 
 309     function go(into, using, key, src) {
 310         if (Array.isArray(into)) {
 311             return into.map((x, i) => go(x, using, i, into));
 312         }
 313 
 314         if (typeof into === 'object') {
 315             const kv = {};
 316             for (const k in into) {
 317                 if (Object.hasOwnProperty.call(into, k)) {
 318                     kv[k] = go(into[k], using, k, into);
 319                 }
 320             }
 321             return kv;
 322         }
 323 
 324         return using(into, key, src);
 325     }
 326 
 327     if (typeof into === 'function') {
 328         return go(using, into, null, into);
 329     }
 330     return go(into, using, null, into);
 331 }
 332 
 333 function divebin(x, y, using) {
 334     if (using == null) {
 335         throw 'divebin: no diving func given';
 336     }
 337 
 338     if (typeof x === 'function') {
 339         return divebin(y, using, x)
 340     }
 341 
 342     switch (using.length) {
 343     case 2:
 344         return dive(x, a => dive(y, b => using(a, b)));
 345     case 4:
 346         return dive(x, (a, i) => dive(y, (b, j) => using(a, i, b, j)));
 347     default:
 348         throw `divebin(...) only supports funcs with 2 or 4 args`;
 349     }
 350 }
 351 
 352 const bindive = divebin;
 353 
 354 function ints(start, stop, f = identity) {
 355     return iota(stop - start + 1, n => f(n + start));
 356 }
 357 
 358 function iota(n, f = identity) {
 359     if (n < 1 || isNaN(n) || !isFinite(n)) {
 360         return [];
 361     }
 362 
 363     n = Math.floor(n);
 364     const range = new Array(n);
 365     for (let i = 0; i < n; i++) {
 366         range[i] = f(i + 1);
 367     }
 368     return range;
 369 }
 370 
 371 function join(what, sep = '') {
 372     if (Array.isArray(what) && Array.isArray(sep)) {
 373         const kv = {};
 374         for (let i = 0; i < what.length; i++) {
 375             kv[what[i]] = i < sep.length ? sep[i] : null;
 376         }
 377         return kv;
 378     }
 379 
 380     if (typeof what === 'string') {
 381         return sep.join(what);
 382     }
 383     return what.join(sep);
 384 }
 385 
 386 function rescue(attempt, fallback) {
 387     try {
 388         return attempt()
 389     } catch (e) {
 390         if (typeof fallback !== 'function') {
 391             return fallback;
 392         }
 393         return fallback.length === 0 ? fallback() : fallback(e);
 394     }
 395 }
 396 
 397 const recover = rescue;
 398 
 399 
 400 main();