File: tjn.js 1 #!/usr/bin/node 2 3 /* 4 The MIT License (MIT) 5 6 Copyright © 2024 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 try { 122 if (useInput && process.argv.length > srcIndex + 2) { 123 throw `multiple named inputs not supported`; 124 } 125 126 const args = process.argv.slice(srcIndex + 1); 127 const expr = process.argv[srcIndex]; 128 const show = json0 ? showJSON0 : showJSON; 129 globalThis.now = new Date(); 130 globalThis.args = args; 131 132 // ignore broken-pipe-style errors, making piping output to the 133 // likes of `head` just work as intended 134 process.stdout.on('error', _ => { process.exit(0); }); 135 136 if (!useInput) { 137 show(run(null, expr)); 138 return; 139 } 140 141 if (process.argv.length === srcIndex + 2) { 142 const name = process.argv[process.argv.length - 1]; 143 144 if (name.startsWith('https://') || name.startsWith('http://')) { 145 fetch(name).then(resp => resp.json()).then(data => { 146 show(run(data, expr)); 147 }); 148 return; 149 } 150 151 if (name === '-') { 152 show(run(JSON.parse(readFileSync(0)), expr)); 153 return; 154 } 155 156 let path = name; 157 if (name.startsWith('file://')) { 158 path = name.slice('file://'.length); 159 } 160 show(run(JSON.parse(readFileSync(path)), expr)); 161 return; 162 } 163 164 show(run(JSON.parse(readFileSync(0)), expr)); 165 } catch (error) { 166 process.stderr.write(`\x1b[31m${error}\x1b[0m\n`); 167 process.exit(1); 168 } 169 } 170 171 function conform(x) { 172 if (x == null) { 173 return x; 174 } 175 176 if (Array.isArray(x)) { 177 return x.filter(e => !(e instanceof Skip)).map(e => conform(e)); 178 } 179 180 switch (typeof x) { 181 case 'bigint': 182 return x.toString(); 183 184 case 'boolean': 185 case 'number': 186 case 'string': 187 return x; 188 189 case 'object': 190 const kv = {}; 191 for (const k in x) { 192 if (Object.prototype.hasOwnProperty.call(x, k)) { 193 const v = kv[k]; 194 if (!(v instanceof Skip)) { 195 kv[k] = conform(v); 196 } 197 } 198 } 199 return kv; 200 201 default: 202 return x.toString(); 203 } 204 } 205 206 function conforms(x) { 207 if (x == null) { 208 return true; 209 } 210 211 if (x instanceof Skip) { 212 return false; 213 } 214 215 if (Array.isArray(x)) { 216 for (const e of x) { 217 if (!conforms(e)) { 218 return false; 219 } 220 } 221 return true; 222 } 223 224 switch (typeof x) { 225 case 'boolean': 226 case 'number': 227 case 'string': 228 return true; 229 230 case 'object': 231 for (const k in x) { 232 if (Object.prototype.hasOwnProperty.call(x, k)) { 233 if (!conforms(x[k])) { 234 return false; 235 } 236 } 237 } 238 return true; 239 240 default: 241 return false; 242 } 243 } 244 245 function showJSON(data) { 246 process.stdout.write(JSON.stringify(data, null, 2)); 247 process.stdout.write('\n'); 248 } 249 250 function showJSON0(data) { 251 process.stdout.write(JSON.stringify(data, null, 0)); 252 process.stdout.write('\n'); 253 } 254 255 function run(data, expr) { 256 switch (expr.trim()) { 257 case '', '.': 258 return data; 259 } 260 261 const d = data; 262 const v = data; 263 const value = data; 264 265 let res = eval(expr); 266 if (typeof res === 'function') { 267 res = res(data); 268 } 269 return conforms(res) ? res : conform(res); 270 } 271 272 273 class Skip { } 274 275 const skip = new Skip(); 276 277 const bquo = '`'; 278 const bquote = '`'; 279 const cr = '\r'; 280 const crlf = '\r\n'; 281 const dquo = '"'; 282 const dquote = '"'; 283 const lcurly = '{'; 284 const rcurly = '}'; 285 const squo = '\''; 286 const squote = '\''; 287 288 289 function chunk(what, chunkSize) { 290 const chunks = []; 291 for (let i = 0; i < what.length; i += chunkSize) { 292 const end = Math.min(i + chunkSize, what.length); 293 chunks.push(what.slice(i, end)); 294 } 295 return chunks; 296 } 297 298 function cond(...args) { 299 for (let i = 0; i < args.length - args.length % 2; i += 2) { 300 if (args[i]) { 301 return args[i + 1]; 302 } 303 } 304 return len(args) % 2 === 0 ? null : args[args.length - 1]; 305 } 306 307 function dive(into, using) { 308 if (using == null) { 309 throw 'dive: no diving func given'; 310 } 311 312 function go(into, using, key, src) { 313 if (Array.isArray(into)) { 314 return into.map((x, i) => go(x, using, i, into)); 315 } 316 317 if (typeof into === 'object') { 318 const kv = {}; 319 for (const k in into) { 320 if (Object.hasOwnProperty.call(into, k)) { 321 kv[k] = go(into[k], using, k, into); 322 } 323 } 324 return kv; 325 } 326 327 return using(into, key, src); 328 } 329 330 if (typeof into === 'function') { 331 return go(using, into, null, into); 332 } 333 return go(into, using, null, into); 334 } 335 336 function divebin(x, y, using) { 337 if (using == null) { 338 throw 'divebin: no diving func given'; 339 } 340 341 if (typeof x === 'function') { 342 return divebin(y, using, x) 343 } 344 345 switch (using.length) { 346 case 2: 347 return dive(x, a => dive(y, b => using(a, b))); 348 case 4: 349 return dive(x, (a, i) => dive(y, (b, j) => using(a, i, b, j))); 350 default: 351 throw `divebin(...) only supports funcs with 2 or 4 args`; 352 } 353 } 354 355 const bindive = divebin; 356 357 function ints(start, stop, f = identity) { 358 return iota(stop - start + 1, n => f(n + start)); 359 } 360 361 function iota(n, f = identity) { 362 if (n < 1 || isNaN(n) || !isFinite(n)) { 363 return []; 364 } 365 366 n = Math.floor(n); 367 const range = new Array(n); 368 for (let i = 0; i < n; i++) { 369 range[i] = f(i + 1); 370 } 371 return range; 372 } 373 374 function join(what, sep = '') { 375 if (Array.isArray(what) && Array.isArray(sep)) { 376 const kv = {}; 377 for (let i = 0; i < what.length; i++) { 378 kv[what[i]] = i < sep.length ? sep[i] : null; 379 } 380 return kv; 381 } 382 383 if (typeof what === 'string') { 384 return sep.join(what); 385 } 386 return what.join(sep); 387 } 388 389 function rescue(attempt, fallback) { 390 try { 391 return attempt() 392 } catch (e) { 393 if (typeof fallback !== 'function') { 394 return fallback; 395 } 396 return fallback.length === 0 ? fallback() : fallback(e); 397 } 398 } 399 400 const recover = rescue; 401 402 403 main();