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