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