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();