#!/usr/bin/ruby # The MIT License (MIT) # # Copyright © 2024 pacman64 # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. info = <<-EOF tlr [options...] [ruby expression] [filepaths/URIs...] Transform Lines with Ruby runs a Ruby expression on each line of plain-text data: each expression given emits its result as its own line. Each input line is available to the expression as either `line`, or `l`. Lines are always stripped of any trailing end-of-line bytes/sequences. When the expression results in non-string iterable values, a sort of input `amplification` happens for the current input-line, where each item from the result is emitted on its own output line. Dictionaries emit their data as a single JSON line. When a formula's result is the nil value, it emits no output line, which filters-out the current line, the same way empty-iterable results do. Similarly, if the argument before the expression is a single equals sign (a `=`, but without the quotes), no data are read/loaded: the expression is then run only once, effectively acting as a `pure` plain-text generator. Options, where leading double-dashes are also allowed, except for alias `=`: -h show this help message -help same as -h -nil don't read any input, and run the expression only once -no-input same as -nil -noinput same as -nil -none same as -nil -null same as -nil = same as -nil -t show a full traceback of this script for exceptions -trace same as -t -traceback same as -t Examples # numbers from 0 to 5, each on its own output line; no input is read/used tlr = '0..5' # all powers up to the 4th, using each input line auto-parsed into a `float` tlr = '1..5' | tlr '(1..4).collect { |p| l.to_f**p }' # separate input lines with an empty line between each; global var `empty` # can be used to avoid bothering with nested shell-quoting tlr = '0..5' | tlr 'i > 0 ? ["", l] : l' # ignore errors/exceptions, in favor of the original lines/values tlr = '["abc", "123"]' | tlr 'begin 2 * line.to_f rescue line end' # ignore errors/exceptions, calling a fallback func with the exception tlr = '["abc", "123"]' | tlr 'begin 2 * line.to_f rescue => e e.to_str end' # filtering lines out via nil values head -c 1024 /dev/urandom | strings | tlr 'l if l.length < 20 else nil' # boolean-valued results are concise ways to filter lines out head -c 1024 /dev/urandom | strings | tlr 'l.length < 20' EOF if ARGV.any? { |e| ['-h', '--h', '-help', '--help'].include?(e) } STDERR.print(info) exit(0) end require 'json' require 'random/formatter' require 'set' require 'uri' def self.open(name, &block) throw 'global function `open` is disabled' end def fix_result(res, line) if res == true return line end if res == false return nil end if res.respond_to?(:call) res = res(line) end # if res.class == Hash # can_go_deep = res.respond_to?(:deep_stringify_keys) # return can_go_deep ? res.deep_stringify_keys : res.stringify_keys # end return res end def show_result(res) if res.nil? return end if res.class == Hash JSON.dump(res, STDOUT, allow_nan=false) STDOUT.print("\n") return end if res.respond_to?(:each) res.each { |x| puts x } return end puts res end def main() expression = nil load_input = true traceback = false input_names = [] no_input_opts = [ '=', '-nil', '--nil', '-none', '--none', '-null', '--null', '-noinput', '-noinput', '-no-input', '--no-input', ] traceback_opts = [ '-t', '--t', '-trace', '--trace', '-traceback', '--traceback', ] ARGV.each do |e| if no_input_opts.include?(e) load_input = false elsif traceback_opts.include?(e) traceback = true elsif expression.nil? expression = e else input_names.push(e) end end if expression.nil? STDERR.print("\x1b[31mno transformation expression given\x1b[0m\n") exit(1) end if expression.strip == '.' expression = nil end def run_expr(expr, value) if expr.nil? show_result(value) return end # using the global vars env because of all the extras defined in it TOPLEVEL_BINDING.local_variable_set(:l, value) TOPLEVEL_BINDING.local_variable_set(:line, value) res = TOPLEVEL_BINDING.eval(expr) i = TOPLEVEL_BINDING.local_variable_get(:i) TOPLEVEL_BINDING.local_variable_set(:i, i + 1) c = TOPLEVEL_BINDING.local_variable_get(:c) TOPLEVEL_BINDING.local_variable_set(:c, c + 1) res = fix_result(res, value) show_result(res) end begin TOPLEVEL_BINDING.local_variable_set(:i, 0) TOPLEVEL_BINDING.local_variable_set(:c, 1) if !load_input res = TOPLEVEL_BINDING.eval(expression) res = fix_result(res, nil) show_result(res) else require 'open-uri' input_names.each do |name| if name == '-' STDIN.each_line do |line| line.chomp! run_expr(expression, line) end else URI.open(name) do |r| r.each_line do |line| line.chomp! run_expr(expression, line) end end end end if input_names.length == 0 STDIN.each_line do |line| line.chomp! run_expr(expression, line) end end end rescue => e if traceback throw e end STDERR.print("\x1b[31m#{e}\x1b[0m\n") exit(1) end end i = 0 c = 0 l = nil line = nil nihil = nil none = nil null = nil s = '' months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ] monweek = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ] sunweek = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ] module Dottable def method_missing(m, *args, &block) Hash(self)[m] end end phy = { 'kilo': 1_000, 'mega': 1_000_000, 'giga': 1_000_000_000, 'tera': 1_000_000_000_000, 'peta': 1_000_000_000_000_000, 'exa': 1_000_000_000_000_000_000, 'zetta': 1_000_000_000_000_000_000_000, 'c': 299_792_458, 'kcd': 683, 'na': 602214076000000000000000, 'femto': 1e-15, 'pico': 1e-12, 'nano': 1e-9, 'micro': 1e-6, 'milli': 1e-3, 'e': 1.602176634e-19, 'f': 96_485.33212, 'h': 6.62607015e-34, 'k': 1.380649e-23, 'mu': 1.66053906892e-27, 'ge': 9.7803267715, 'gn': 9.80665, } phy.extend(Dottable) physics = phy # using literal strings on the cmd-line is often tricky/annoying: some of # these aliases can help get around multiple levels of string-quoting; no # quotes are needed as the script will later make these values accessible # via the property/dot syntax sym = { 'amp': '&', 'ampersand': '&', 'ansiclear': '\x1b[0m', 'ansinormal': '\x1b[0m', 'ansireset': '\x1b[0m', 'apo': '\'', 'apos': '\'', 'ast': '*', 'asterisk': '*', 'at': '@', 'backquote': '`', 'backslash': '\\', 'backtick': '`', 'ball': '●', 'bang': '!', 'bigsigma': 'Σ', 'block': '█', 'bquo': '`', 'bquote': '`', 'bslash': '\\', 'bullet': '•', 'caret': '^', 'cdot': '·', 'circle': '●', 'colon': ':', 'comma': ',', 'cr': '\r', 'crlf': '\r\n', 'cross': '×', 'cs': ', ', # 'dash': '–', 'dash': '—', 'dollar': '$', 'dot': '.', 'dquo': '"', 'dquote': '"', 'emark': '!', 'empty': '', 'eq': '=', 'et': '&', 'euro': '€', 'ge': '≥', 'geq': '≥', 'gt': '>', 'hellip': '…', 'hole': '○', 'infinity': '∞', 'le': '≤', 'leq': '≤', 'lf': '\n', 'lt': '<', 'mdash': '—', 'mdot': '·', 'miniball': '•', 'minus': '-', 'ndash': '–', 'neq': '≠', 'perc': '%', 'percent': '%', 'period': '.', 'plus': '+', 'qmark': '?', 'que': '?', 'sball': '•', 'semi': ';', 'semicolon': ';', 'sharp': '#', 'slash': '/', 'space': ' ', 'square': '■', 'squo': '\'', 'squote': '\'', 'tab': '\t', 'tilde': '~', 'underscore': '_', 'uscore': '_', 'utf8bom': '\xef\xbb\xbf', } sym.extend(Dottable) symbols = sym units = { 'cup2l': 0.23658824, 'floz2l': 0.0295735295625, 'floz2ml': 29.5735295625, 'ft2m': 0.3048, 'gal2l': 3.785411784, 'in2cm': 2.54, 'lb2kg': 0.45359237, 'mi2km': 1.609344, 'mpg2kpl': 0.425143707, 'nmi2km': 1.852, 'oz2g': 28.34952312, 'psi2pa': 6894.757293168, 'ton2kg': 907.18474, 'yd2m': 0.9144, 'mol': 602214076000000000000000, 'mole': 602214076000000000000000, 'hour': 3_600, 'day': 86_400, 'week': 604_800, 'hr': 3_600, 'wk': 604_800, 'kb': 1024, 'mb': 1024**2, 'gb': 1024**3, 'tb': 1024**4, 'pb': 1024**5, } units.extend(Dottable) main()