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)