File: tlr.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 tlr [options...] [ruby expression] [filepaths/URIs...]
  28 
  29 
  30 Transform Lines with Ruby runs a Ruby expression on each line of plain-text
  31 data: each expression given emits its result as its own line. Each input line
  32 is available to the expression as either `line`, or `l`. Lines are always
  33 stripped of any trailing end-of-line bytes/sequences.
  34 
  35 When the expression results in non-string iterable values, a sort of input
  36 `amplification` happens for the current input-line, where each item from
  37 the result is emitted on its own output line. Dictionaries emit their data
  38 as a single JSON line.
  39 
  40 When a formula's result is the nil value, it emits no output line, which
  41 filters-out the current line, the same way empty-iterable results do.
  42 
  43 Similarly, if the argument before the expression is a single equals sign
  44 (a `=`, but without the quotes), no data are read/loaded: the expression is
  45 then run only once, effectively acting as a `pure` plain-text generator.
  46 
  47 Options, where leading double-dashes are also allowed, except for alias `=`:
  48 
  49     -h           show this help message
  50     -help        same as -h
  51 
  52     -nil         don't read any input, and run the expression only once
  53     -no-input    same as -nil
  54     -noinput     same as -nil
  55     -none        same as -nil
  56     -null        same as -nil
  57     =            same as -nil
  58 
  59     -t           show a full traceback of this script for exceptions
  60     -trace       same as -t
  61     -traceback   same as -t
  62 
  63 
  64 Examples
  65 
  66 # numbers from 0 to 5, each on its own output line; no input is read/used
  67 tlr = '0..5'
  68 
  69 # all powers up to the 4th, using each input line auto-parsed into a `float`
  70 tlr = '1..5' | tlr '(1..4).collect { |p| l.to_f**p }'
  71 
  72 # separate input lines with an empty line between each; global var `empty`
  73 # can be used to avoid bothering with nested shell-quoting
  74 tlr = '0..5' | tlr 'i > 0 ? ["", l] : l'
  75 
  76 # ignore errors/exceptions, in favor of the original lines/values
  77 tlr = '["abc", "123"]' | tlr 'begin 2 * line.to_f rescue line end'
  78 
  79 # ignore errors/exceptions, calling a fallback func with the exception
  80 tlr = '["abc", "123"]' | tlr 'begin 2 * line.to_f rescue => e e.to_str end'
  81 
  82 # filtering lines out via nil values
  83 head -c 1024 /dev/urandom | strings | tlr 'l if l.length < 20 else nil'
  84 
  85 # boolean-valued results are concise ways to filter lines out
  86 head -c 1024 /dev/urandom | strings | tlr 'l.length < 20'
  87 EOF
  88 
  89 
  90 if ARGV.any? { |e| ['-h', '--h', '-help', '--help'].include?(e) }
  91     STDERR.print(info)
  92     exit(0)
  93 end
  94 
  95 
  96 require 'json'
  97 require 'random/formatter'
  98 require 'set'
  99 require 'uri'
 100 
 101 
 102 def self.open(name, &block)
 103     throw 'global function `open` is disabled'
 104 end
 105 
 106 
 107 def fix_result(res, line)
 108     if res == true
 109         return line
 110     end
 111     if res == false
 112         return nil
 113     end
 114 
 115     if res.respond_to?(:call)
 116         res = res(line)
 117     end
 118 
 119     # if res.class == Hash
 120     #     can_go_deep = res.respond_to?(:deep_stringify_keys)
 121     #     return can_go_deep ? res.deep_stringify_keys : res.stringify_keys
 122     # end
 123 
 124     return res
 125 end
 126 
 127 
 128 def show_result(res)
 129     if res.nil?
 130         return
 131     end
 132 
 133     if res.class == Hash
 134         JSON.dump(res, STDOUT, allow_nan=false)
 135         STDOUT.print("\n")
 136         return
 137     end
 138 
 139     if res.respond_to?(:each)
 140         res.each { |x| puts x }
 141         return
 142     end
 143 
 144     puts res
 145 end
 146 
 147 
 148 def main()
 149     expression = nil
 150     load_input = true
 151     traceback = false
 152     input_names = []
 153 
 154     no_input_opts = [
 155         '=', '-nil', '--nil', '-none', '--none', '-null', '--null',
 156         '-noinput', '-noinput', '-no-input', '--no-input',
 157     ]
 158     traceback_opts = [
 159         '-t', '--t', '-trace', '--trace', '-traceback', '--traceback',
 160     ]
 161 
 162     ARGV.each do |e|
 163         if no_input_opts.include?(e)
 164             load_input = false
 165         elsif traceback_opts.include?(e)
 166             traceback = true
 167         elsif expression.nil?
 168             expression = e
 169         else
 170             input_names.push(e)
 171         end
 172     end
 173 
 174     if expression.nil?
 175         STDERR.print("\x1b[31mno transformation expression given\x1b[0m\n")
 176         exit(1)
 177     end
 178 
 179     if expression.strip == '.'
 180         expression = nil
 181     end
 182 
 183     def run_expr(expr, value)
 184         if expr.nil?
 185             show_result(value)
 186             return
 187         end
 188 
 189         # using the global vars env because of all the extras defined in it
 190         TOPLEVEL_BINDING.local_variable_set(:l, value)
 191         TOPLEVEL_BINDING.local_variable_set(:line, value)
 192         res = TOPLEVEL_BINDING.eval(expr)
 193         i = TOPLEVEL_BINDING.local_variable_get(:i)
 194         TOPLEVEL_BINDING.local_variable_set(:i, i + 1)
 195         c = TOPLEVEL_BINDING.local_variable_get(:c)
 196         TOPLEVEL_BINDING.local_variable_set(:c, c + 1)
 197 
 198         res = fix_result(res, value)
 199         show_result(res)
 200     end
 201 
 202     begin
 203         TOPLEVEL_BINDING.local_variable_set(:i, 0)
 204         TOPLEVEL_BINDING.local_variable_set(:c, 1)
 205 
 206         if !load_input
 207             res = TOPLEVEL_BINDING.eval(expression)
 208             res = fix_result(res, nil)
 209             show_result(res)
 210         else
 211             require 'open-uri'
 212 
 213             input_names.each do |name|
 214                 if name == '-'
 215                     STDIN.each_line do |line|
 216                         line.chomp!
 217                         run_expr(expression, line)
 218                     end
 219                 else
 220                     URI.open(name) do |r|
 221                         r.each_line do |line|
 222                             line.chomp!
 223                             run_expr(expression, line)
 224                         end
 225                     end
 226                 end
 227             end
 228 
 229             if input_names.length == 0
 230                 STDIN.each_line do |line|
 231                     line.chomp!
 232                     run_expr(expression, line)
 233                 end
 234             end
 235         end
 236     rescue => e
 237         if traceback
 238             throw e
 239         end
 240         STDERR.print("\x1b[31m#{e}\x1b[0m\n")
 241         exit(1)
 242     end
 243 end
 244 
 245 
 246 i = 0
 247 c = 0
 248 l = nil
 249 line = nil
 250 
 251 nihil = nil
 252 none = nil
 253 null = nil
 254 s = ''
 255 
 256 months = [
 257     'January', 'February', 'March', 'April', 'May', 'June',
 258     'July', 'August', 'September', 'October', 'November', 'December',
 259 ]
 260 
 261 monweek = [
 262     'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
 263     'Saturday', 'Sunday',
 264 ]
 265 
 266 sunweek = [
 267     'Sunday',
 268     'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
 269 ]
 270 
 271 module Dottable
 272     def method_missing(m, *args, &block)
 273         Hash(self)[m]
 274     end
 275 end
 276 
 277 phy = {
 278     'kilo': 1_000,
 279     'mega': 1_000_000,
 280     'giga': 1_000_000_000,
 281     'tera': 1_000_000_000_000,
 282     'peta': 1_000_000_000_000_000,
 283     'exa': 1_000_000_000_000_000_000,
 284     'zetta': 1_000_000_000_000_000_000_000,
 285 
 286     'c': 299_792_458,
 287     'kcd': 683,
 288     'na': 602214076000000000000000,
 289 
 290     'femto': 1e-15,
 291     'pico': 1e-12,
 292     'nano': 1e-9,
 293     'micro': 1e-6,
 294     'milli': 1e-3,
 295 
 296     'e': 1.602176634e-19,
 297     'f': 96_485.33212,
 298     'h': 6.62607015e-34,
 299     'k': 1.380649e-23,
 300     'mu': 1.66053906892e-27,
 301 
 302     'ge': 9.7803267715,
 303     'gn': 9.80665,
 304 }
 305 
 306 phy.extend(Dottable)
 307 
 308 physics = phy
 309 
 310 # using literal strings on the cmd-line is often tricky/annoying: some of
 311 # these aliases can help get around multiple levels of string-quoting; no
 312 # quotes are needed as the script will later make these values accessible
 313 # via the property/dot syntax
 314 sym = {
 315     'amp': '&',
 316     'ampersand': '&',
 317     'ansiclear': '\x1b[0m',
 318     'ansinormal': '\x1b[0m',
 319     'ansireset': '\x1b[0m',
 320     'apo': '\'',
 321     'apos': '\'',
 322     'ast': '*',
 323     'asterisk': '*',
 324     'at': '@',
 325     'backquote': '`',
 326     'backslash': '\\',
 327     'backtick': '`',
 328     'ball': '',
 329     'bang': '!',
 330     'bigsigma': 'Σ',
 331     'block': '',
 332     'bquo': '`',
 333     'bquote': '`',
 334     'bslash': '\\',
 335     'bullet': '',
 336     'caret': '^',
 337     'cdot': '·',
 338     'circle': '',
 339     'colon': ':',
 340     'comma': ',',
 341     'cr': '\r',
 342     'crlf': '\r\n',
 343     'cross': '×',
 344     'cs': ', ',
 345     # 'dash': '–',
 346     'dash': '',
 347     'dollar': '$',
 348     'dot': '.',
 349     'dquo': '"',
 350     'dquote': '"',
 351     'emark': '!',
 352     'empty': '',
 353     'eq': '=',
 354     'et': '&',
 355     'euro': '',
 356     'ge': '',
 357     'geq': '',
 358     'gt': '>',
 359     'hellip': '',
 360     'hole': '',
 361     'infinity': '',
 362     'le': '',
 363     'leq': '',
 364     'lf': '\n',
 365     'lt': '<',
 366     'mdash': '',
 367     'mdot': '·',
 368     'miniball': '',
 369     'minus': '-',
 370     'ndash': '',
 371     'neq': '',
 372     'perc': '%',
 373     'percent': '%',
 374     'period': '.',
 375     'plus': '+',
 376     'qmark': '?',
 377     'que': '?',
 378     'sball': '',
 379     'semi': ';',
 380     'semicolon': ';',
 381     'sharp': '#',
 382     'slash': '/',
 383     'space': ' ',
 384     'square': '',
 385     'squo': '\'',
 386     'squote': '\'',
 387     'tab': '\t',
 388     'tilde': '~',
 389     'underscore': '_',
 390     'uscore': '_',
 391     'utf8bom': '\xef\xbb\xbf',
 392 }
 393 
 394 sym.extend(Dottable)
 395 
 396 symbols = sym
 397 
 398 units = {
 399     'cup2l': 0.23658824,
 400     'floz2l': 0.0295735295625,
 401     'floz2ml': 29.5735295625,
 402     'ft2m': 0.3048,
 403     'gal2l': 3.785411784,
 404     'in2cm': 2.54,
 405     'lb2kg': 0.45359237,
 406     'mi2km': 1.609344,
 407     'mpg2kpl': 0.425143707,
 408     'nmi2km': 1.852,
 409     'oz2g': 28.34952312,
 410     'psi2pa': 6894.757293168,
 411     'ton2kg': 907.18474,
 412     'yd2m': 0.9144,
 413 
 414     'mol': 602214076000000000000000,
 415     'mole': 602214076000000000000000,
 416 
 417     'hour': 3_600,
 418     'day': 86_400,
 419     'week': 604_800,
 420 
 421     'hr': 3_600,
 422     'wk': 604_800,
 423 
 424     'kb': 1024,
 425     'mb': 1024**2,
 426     'gb': 1024**3,
 427     'tb': 1024**4,
 428     'pb': 1024**5,
 429 }
 430 
 431 units.extend(Dottable)
 432 
 433 
 434 main()