File: tjr.rb 1 #!/usr/bin/ruby 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2024 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the “Software”), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 info = <<-EOF 27 tjr [options...] [ruby expression] [filepath/URI...] 28 29 30 Transform Json with Ruby loads JSON data, runs a Ruby expression on it, and 31 emits the result as JSON. Parsed input-data are available to the expression 32 as any of the variables named `v`, `value`, `d`, and `data`. 33 34 If no file/URI is given, it loads JSON data from its standard input. If the 35 argument before the expression is a single equals sign (a `=`, without the 36 quotes), no data are read/parsed, and the expression is evaluated as given. 37 38 Options, where leading double-dashes are also allowed, except for alias `=`: 39 40 -c compact single-line JSON output (JSON-0) 41 -compact same as -c 42 -j0 same as -c 43 -json0 same as -c 44 -json-0 same as -c 45 46 -h show this help message 47 -help same as -h 48 49 -nil don't read any input 50 -no-input same as -nil 51 -noinput same as -nil 52 -none same as -nil 53 -null same as -nil 54 = same as -nil 55 56 -t show a full traceback of this script for exceptions 57 -trace same as -t 58 -traceback same as -t 59 EOF 60 61 62 if ARGV.any? { |e| ['-h', '--h', '-help', '--help'].include?(e) } 63 STDERR.print(info) 64 exit(0) 65 end 66 67 68 require 'json' 69 require 'random/formatter' 70 require 'set' 71 require 'uri' 72 73 74 def self.open(name, &block) 75 throw 'global function `open` is disabled' 76 end 77 78 79 def load_json(name) 80 require 'open-uri' 81 if name == '-' 82 return JSON.load(STDIN, allow_nan=false) 83 else 84 return URI.open(name) { |r| JSON.load(r, allow_nan=false) } 85 end 86 end 87 88 89 def fix_result(res, value) 90 if res.respond_to?(:call) 91 return res(value) 92 end 93 94 # if res.class == Hash 95 # can_go_deep = res.respond_to?(:deep_stringify_keys) 96 # return can_go_deep ? res.deep_stringify_keys : res.stringify_keys 97 # end 98 99 return res 100 end 101 102 103 def main() 104 expression = nil 105 load_input = true 106 compact = false 107 traceback = false 108 input_name = nil 109 110 no_input_opts = [ 111 '=', '-nil', '--nil', '-none', '--none', '-null', '--null', 112 '-noinput', '-noinput', '-no-input', '--no-input', 113 ] 114 compact_opts = [ 115 '-c', '--c', '-j0', '--j0', '-json0', '--json0', '-json-0', '--json-0', 116 ] 117 traceback_opts = [ 118 '-t', '--t', '-trace', '--trace', '-traceback', '--traceback', 119 ] 120 121 ARGV.each do |e| 122 if no_input_opts.include?(e) 123 load_input = false 124 elsif compact_opts.include?(e) 125 compact = true 126 elsif traceback_opts.include?(e) 127 traceback = true 128 elsif expression.nil? 129 expression = e 130 elsif input_name.nil? 131 input_name = e 132 else 133 STDERR.print("\x1b[31mgiven too many arguments\x1b[0m\n") 134 exit(1) 135 end 136 end 137 138 if expression.nil? 139 STDERR.print("\x1b[31mno transformation expression given\x1b[0m\n") 140 exit(1) 141 end 142 143 begin 144 if !load_input 145 data = nil 146 elsif input_name.nil? || input_name == '-' 147 data = JSON.load(STDIN, allow_nan=false) 148 else 149 data = load_json(input_name) 150 end 151 152 # using the global vars env because of all the extras defined in it 153 TOPLEVEL_BINDING.local_variable_set(:d, data) 154 TOPLEVEL_BINDING.local_variable_set(:data, data) 155 TOPLEVEL_BINDING.local_variable_set(:v, data) 156 TOPLEVEL_BINDING.local_variable_set(:val, data) 157 TOPLEVEL_BINDING.local_variable_set(:value, data) 158 159 if expression.strip != '.' 160 data = fix_result(TOPLEVEL_BINDING.eval(expression), data) 161 end 162 163 if compact 164 JSON.dump(data, STDOUT, allow_nan=false) 165 else 166 STDOUT.print(JSON.pretty_unparse(data, allow_nan=false)) 167 end 168 STDOUT.print("\n") 169 rescue => e 170 if traceback 171 throw e 172 end 173 STDERR.print("\x1b[31m#{e}\x1b[0m\n") 174 exit(1) 175 end 176 end 177 178 179 d = nil 180 data = nil 181 v = nil 182 val = nil 183 value = nil 184 185 nihil = nil 186 none = nil 187 null = nil 188 s = '' 189 190 months = [ 191 'January', 'February', 'March', 'April', 'May', 'June', 192 'July', 'August', 'September', 'October', 'November', 'December', 193 ] 194 195 monweek = [ 196 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 197 'Saturday', 'Sunday', 198 ] 199 200 sunweek = [ 201 'Sunday', 202 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 203 ] 204 205 module Dottable 206 def method_missing(m, *args, &block) 207 Hash(self)[m] 208 end 209 end 210 211 phy = { 212 'kilo': 1_000, 213 'mega': 1_000_000, 214 'giga': 1_000_000_000, 215 'tera': 1_000_000_000_000, 216 'peta': 1_000_000_000_000_000, 217 'exa': 1_000_000_000_000_000_000, 218 'zetta': 1_000_000_000_000_000_000_000, 219 220 'c': 299_792_458, 221 'kcd': 683, 222 'na': 602214076000000000000000, 223 224 'femto': 1e-15, 225 'pico': 1e-12, 226 'nano': 1e-9, 227 'micro': 1e-6, 228 'milli': 1e-3, 229 230 'e': 1.602176634e-19, 231 'f': 96_485.33212, 232 'h': 6.62607015e-34, 233 'k': 1.380649e-23, 234 'mu': 1.66053906892e-27, 235 236 'ge': 9.7803267715, 237 'gn': 9.80665, 238 } 239 240 phy.extend(Dottable) 241 242 physics = phy 243 244 # using literal strings on the cmd-line is often tricky/annoying: some of 245 # these aliases can help get around multiple levels of string-quoting; no 246 # quotes are needed as the script will later make these values accessible 247 # via the property/dot syntax 248 sym = { 249 'amp': '&', 250 'ampersand': '&', 251 'ansiclear': '\x1b[0m', 252 'ansinormal': '\x1b[0m', 253 'ansireset': '\x1b[0m', 254 'apo': '\'', 255 'apos': '\'', 256 'ast': '*', 257 'asterisk': '*', 258 'at': '@', 259 'backquote': '`', 260 'backslash': '\\', 261 'backtick': '`', 262 'ball': '●', 263 'bang': '!', 264 'bigsigma': 'Σ', 265 'block': '█', 266 'bquo': '`', 267 'bquote': '`', 268 'bslash': '\\', 269 'bullet': '•', 270 'caret': '^', 271 'cdot': '·', 272 'circle': '●', 273 'colon': ':', 274 'comma': ',', 275 'cr': '\r', 276 'crlf': '\r\n', 277 'cross': '×', 278 'cs': ', ', 279 # 'dash': '–', 280 'dash': '—', 281 'dollar': '$', 282 'dot': '.', 283 'dquo': '"', 284 'dquote': '"', 285 'emark': '!', 286 'empty': '', 287 'eq': '=', 288 'et': '&', 289 'euro': '€', 290 'ge': '≥', 291 'geq': '≥', 292 'gt': '>', 293 'hellip': '…', 294 'hole': '○', 295 'infinity': '∞', 296 'le': '≤', 297 'leq': '≤', 298 'lf': '\n', 299 'lt': '<', 300 'mdash': '—', 301 'mdot': '·', 302 'miniball': '•', 303 'minus': '-', 304 'ndash': '–', 305 'neq': '≠', 306 'perc': '%', 307 'percent': '%', 308 'period': '.', 309 'plus': '+', 310 'qmark': '?', 311 'que': '?', 312 'sball': '•', 313 'semi': ';', 314 'semicolon': ';', 315 'sharp': '#', 316 'slash': '/', 317 'space': ' ', 318 'square': '■', 319 'squo': '\'', 320 'squote': '\'', 321 'tab': '\t', 322 'tilde': '~', 323 'underscore': '_', 324 'uscore': '_', 325 'utf8bom': '\xef\xbb\xbf', 326 } 327 328 sym.extend(Dottable) 329 330 symbols = sym 331 332 units = { 333 'cup2l': 0.23658824, 334 'floz2l': 0.0295735295625, 335 'floz2ml': 29.5735295625, 336 'ft2m': 0.3048, 337 'gal2l': 3.785411784, 338 'in2cm': 2.54, 339 'lb2kg': 0.45359237, 340 'mi2km': 1.609344, 341 'mpg2kpl': 0.425143707, 342 'nmi2km': 1.852, 343 'oz2g': 28.34952312, 344 'psi2pa': 6894.757293168, 345 'ton2kg': 907.18474, 346 'yd2m': 0.9144, 347 348 'mol': 602214076000000000000000, 349 'mole': 602214076000000000000000, 350 351 'hour': 3_600, 352 'day': 86_400, 353 'week': 604_800, 354 355 'hr': 3_600, 356 'wk': 604_800, 357 358 'kb': 1024, 359 'mb': 1024**2, 360 'gb': 1024**3, 361 'tb': 1024**4, 362 'pb': 1024**5, 363 } 364 365 units.extend(Dottable) 366 367 368 main()