File: tsp.py
   1 #!/usr/bin/python
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 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 = '''
  27 tsp [options...] [python expression] [files/URIs...]
  28 
  29 
  30 Transform Strings with Python runs a python expression on each whole-input,
  31 read as a string value.
  32 
  33 The expression can use either `s`, `v`, `value`, `d`, or `data` for the
  34 current input.
  35 
  36 Input-sources can be either files or web-URIs. When not given any explicit
  37 named sources, the standard input is used. It's even possible to reuse the
  38 standard input using multiple single dashes (-) in the order needed: stdin
  39 is only read once in this case, and kept for later reuse.
  40 
  41 When the expression results in None, the current input is ignored. When the
  42 expression results in a boolean, this determines whether the whole input is
  43 copied/appended back to the standard output, or ignored.
  44 '''
  45 
  46 
  47 from sys import argv, exit, stderr, stdin, stdout
  48 from time import sleep
  49 from typing import Generator
  50 
  51 
  52 if len(argv) < 2:
  53     print(info.strip(), file=stderr)
  54     exit(0)
  55 if len(argv) > 1 and argv[1] in ('-h', '--h', '-help', '--help'):
  56     print(info.strip())
  57     exit(0)
  58 
  59 
  60 class Skip:
  61     pass
  62 
  63 skip = Skip()
  64 
  65 def cond(*args):
  66     if len(args) == 0:
  67         return None
  68 
  69     for i, e in enumerate(args):
  70         if i % 2 == 0 and i < len(args) - 1 and e:
  71             return args[i + 1]
  72 
  73     return args[-1] if len(args) % 2 == 1 else None
  74 
  75 def maybe(f, x):
  76     try:
  77         return f(x)
  78     except Exception as _:
  79         return x
  80 
  81 def number(x):
  82     try:
  83         return int(x)
  84     except Exception as _:
  85         pass
  86     try:
  87         return float(x)
  88     except Exception as _:
  89         return x
  90 
  91 def rescue(attempt, fallback = None):
  92     try:
  93         return attempt()
  94     except BrokenPipeError as e:
  95         raise e
  96     except Exception as e:
  97         if callable(fallback):
  98             return fallback(e)
  99         return fallback
 100 
 101 rescued = rescue
 102 
 103 def wait(seconds, result):
 104     t = (int, float)
 105     if (not isinstance(seconds, t)) and isinstance(result, t):
 106         seconds, result = result, seconds
 107     sleep(seconds)
 108     return result
 109 
 110 delay = wait
 111 
 112 def make_open_read(open):
 113     'Restrict the file-open func to a read-only-binary file-open func.'
 114     def open_read(name):
 115         return open(name, mode='rb')
 116     return open_read
 117 
 118 def fail(msg, code = 1):
 119     print(f'\x1b[31m{str(msg)}\x1b[0m', file=stderr)
 120     exit(code)
 121 
 122 def message(msg, result = None):
 123     print(msg, file=stderr)
 124     return result
 125 
 126 msg = message
 127 
 128 def seemsurl(s):
 129     protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:')
 130     return any(s.startswith(p) for p in protocols)
 131 
 132 def adapt_result(x, default):
 133     if x is True:
 134         return default
 135     if x is False:
 136         return None
 137 
 138     if isinstance(x, Skip):
 139         return None
 140 
 141     if callable(x):
 142         return x(default)
 143     return x
 144 
 145 def emit_result(w, x):
 146     if isinstance(x, (list, tuple, range, Generator)):
 147         for e in x:
 148             emit_simple_value(w, e)
 149     else:
 150         emit_simple_value(w, x)
 151 
 152 def emit_simple_value(w, x):
 153     if x is None:
 154         return
 155     if isinstance(x, int):
 156         x = str(x)
 157     w.write(bytes(x, encoding='utf-8'))
 158 
 159 def eval_expr(expr, using):
 160     global v, val, value, d, dat, data
 161     # offer several aliases for the variable with the input string
 162     s = v = val = value = d = dat = data = using
 163     return adapt_result(eval(expr), using)
 164 
 165 
 166 nil = none = null = None
 167 
 168 
 169 exec = None
 170 open = make_open_read(open)
 171 
 172 no_input_opts = (
 173     '=', '-n', '--n', '-nil', '--nil', '-none', '--none', '-null', '--null',
 174 )
 175 modules_opts = (
 176     '-m', '--m', '-mod', '--mod', '-module', '--module',
 177     '-modules', '--modules',
 178 )
 179 more_modules_opts = ('-mm', '--mm', '-more', '--more')
 180 
 181 args = argv[1:]
 182 load_input = True
 183 expression = None
 184 
 185 while len(args) > 0:
 186     if args[0] == '--':
 187         args = args[1:]
 188         break
 189 
 190     if args[0] in no_input_opts:
 191         load_input = False
 192         args = args[1:]
 193         continue
 194 
 195     if args[0] in no_input_opts:
 196         no_input = True
 197         args = args[1:]
 198         continue
 199 
 200     if args[0] in modules_opts:
 201         try:
 202             if len(args) < 2:
 203                 msg = 'a module name or a comma-separated list of modules'
 204                 raise Exception('expected ' + msg)
 205 
 206             g = globals()
 207             from importlib import import_module
 208             for e in args[1].split(','):
 209                 g[e] = import_module(e)
 210 
 211             g = None
 212             import_module = None
 213             args = args[2:]
 214         except Exception as e:
 215             fail(e, 1)
 216 
 217         continue
 218 
 219     if args[0] in more_modules_opts:
 220         import functools, itertools, json, math, random, statistics, string, time
 221         args = args[1:]
 222         continue
 223 
 224     break
 225 
 226 cr = '\r'
 227 crlf = '\r\n'
 228 dquo = dquote = '"'
 229 empty = ''
 230 lcurly = '{'
 231 lf = '\n'
 232 rcurly = '}'
 233 space = ' '
 234 squo = squote = '\''
 235 tab = '\t'
 236 utf8bom = '\xef\xbb\xbf'
 237 bom = {
 238     'utf8': '\xef\xbb\xbf',
 239     'utf16be': '\xfe\xff',
 240     'utf16le': '\xff\xfe',
 241     'utf32be': '\x00\x00\xfe\xff',
 242     'utf32le': '\xff\xfe\x00\x00',
 243 }
 244 
 245 if len(args) > 0:
 246     expression = args[0]
 247     args = args[1:]
 248 
 249 if expression is None:
 250     print(info.strip(), file=stderr)
 251     exit(0)
 252 
 253 try:
 254     if not expression or expression == '.':
 255         expression = 'data'
 256     expression = compile(expression, expression, 'eval')
 257 
 258     got_stdin = False
 259     all_stdin = None
 260     dashes = args.count('-')
 261 
 262     data = None
 263 
 264     if not load_input:
 265         emit_result(stdout.buffer, eval_expr(expression, None))
 266         exit(0)
 267 
 268     if any(seemsurl(name) for name in args):
 269         from urllib.request import urlopen
 270 
 271     for name in args:
 272         if name == '-':
 273             if dashes > 1:
 274                 if not got_stdin:
 275                     all_stdin = stdin.buffer.read()
 276                     got_stdin = True
 277                 data = all_stdin
 278             else:
 279                 data = stdin.buffer.read()
 280 
 281             data = s = str(data, encoding='utf-8')
 282         elif seemsurl(name):
 283             with urlopen(name) as inp:
 284                 data = inp.read()
 285         else:
 286             with open(name) as inp:
 287                 data = inp.read()
 288 
 289         data = s = str(data, encoding='utf-8')
 290         emit_result(stdout.buffer, eval_expr(expression, data))
 291 
 292     if len(args) == 0:
 293         data = stdin.buffer.read()
 294         data = s = str(data, encoding='utf-8')
 295         emit_result(stdout.buffer, eval_expr(expression, data))
 296 except BrokenPipeError:
 297     # quit quietly, instead of showing a confusing error message
 298     stderr.close()
 299     exit(0)
 300 except KeyboardInterrupt:
 301     exit(2)
 302 except Exception as e:
 303     s = str(e)
 304     s = s if s else '<generic exception>'
 305     print(s, file=stderr)
 306     exit(1)