File: tsp.py 1 #!/usr/bin/python 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 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 rescue(attempt, fallback = None): 76 try: 77 return attempt() 78 except BrokenPipeError as e: 79 raise e 80 except Exception as e: 81 if callable(fallback): 82 return fallback(e) 83 return fallback 84 85 rescued = rescue 86 87 def wait(seconds, result): 88 t = (int, float) 89 if (not isinstance(seconds, t)) and isinstance(result, t): 90 seconds, result = result, seconds 91 sleep(seconds) 92 return result 93 94 delay = wait 95 96 def make_open_read(open): 97 'Restrict the file-open func to a read-only-binary file-open func.' 98 def open_read(name): 99 return open(name, mode='rb') 100 return open_read 101 102 def fail(msg, code = 1): 103 print(f'\x1b[31m{str(msg)}\x1b[0m', file=stderr) 104 exit(code) 105 106 def message(msg, result = None): 107 print(msg, file=stderr) 108 return result 109 110 msg = message 111 112 def seemsurl(s): 113 protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') 114 return any(s.startswith(p) for p in protocols) 115 116 def adapt_result(x, default): 117 if x is True: 118 return default 119 if x is False: 120 return None 121 122 if isinstance(x, Skip): 123 return None 124 125 if callable(x): 126 return x(default) 127 return x 128 129 def emit_result(w, x): 130 if x is None: 131 return 132 133 if isinstance(x, int): 134 w.write(str(x)) 135 return 136 137 if isinstance(x, (list, tuple, range, Generator)): 138 for e in x: 139 w.write(str(e, encoding='utf-8')) 140 return 141 142 w.write(str(x)) 143 144 def eval_expr(expr, using): 145 global v, val, value, d, dat, data 146 # offer several aliases for the variable with the input string 147 s = v = val = value = d = dat = data = using 148 return adapt_result(eval(expr), using) 149 150 151 nil = none = null = None 152 153 154 exec = None 155 open = make_open_read(open) 156 157 no_input_opts = ( 158 '=', '-n', '--n', '-nil', '--nil', '-none', '--none', '-null', '--null', 159 ) 160 modules_opts = ( 161 '-m', '--m', '-mod', '--mod', '-module', '--module', 162 '-modules', '--modules', 163 ) 164 more_modules_opts = ('-mm', '--mm', '-more', '--more') 165 166 args = argv[1:] 167 load_input = True 168 expression = None 169 170 while len(args) > 0: 171 if args[0] == '--': 172 args = args[1:] 173 break 174 175 if args[0] in no_input_opts: 176 load_input = False 177 args = args[1:] 178 continue 179 180 if args[0] in no_input_opts: 181 no_input = True 182 args = args[1:] 183 continue 184 185 if args[0] in modules_opts: 186 try: 187 if len(args) < 2: 188 msg = 'a module name or a comma-separated list of modules' 189 raise Exception('expected ' + msg) 190 191 g = globals() 192 from importlib import import_module 193 for e in args[1].split(','): 194 g[e] = import_module(e) 195 196 g = None 197 import_module = None 198 args = args[2:] 199 except Exception as e: 200 fail(e, 1) 201 202 continue 203 204 if args[0] in more_modules_opts: 205 import functools, itertools, json, math, random, statistics, string, time 206 args = args[1:] 207 continue 208 209 break 210 211 cr = '\r' 212 crlf = '\r\n' 213 dquo = dquote = '"' 214 empty = '' 215 lcurly = '{' 216 lf = '\n' 217 rcurly = '}' 218 space = ' ' 219 squo = squote = '\'' 220 tab = '\t' 221 utf8bom = '\xef\xbb\xbf' 222 bom = { 223 'utf8': '\xef\xbb\xbf', 224 'utf16be': '\xfe\xff', 225 'utf16le': '\xff\xfe', 226 'utf32be': '\x00\x00\xfe\xff', 227 'utf32le': '\xff\xfe\x00\x00', 228 } 229 230 if len(args) > 0: 231 expression = args[0] 232 args = args[1:] 233 234 if expression is None: 235 print(info.strip(), file=stderr) 236 exit(0) 237 238 try: 239 if not expression or expression == '.': 240 expression = 'data' 241 expression = compile(expression, expression, 'eval') 242 243 got_stdin = False 244 all_stdin = None 245 dashes = args.count('-') 246 247 data = None 248 249 if not load_input: 250 emit_result(stdout.buffer, eval_expr(expression, None)) 251 exit(0) 252 253 if any(seemsurl(name) for name in args): 254 from urllib.request import urlopen 255 256 for name in args: 257 if name == '-': 258 if dashes > 1: 259 if not got_stdin: 260 all_stdin = stdin.buffer.read() 261 got_stdin = True 262 data = all_stdin 263 else: 264 data = stdin.buffer.read() 265 266 data = s = str(data, encoding='utf-8') 267 elif seemsurl(name): 268 with urlopen(name) as inp: 269 data = inp.read() 270 else: 271 with open(name) as inp: 272 data = inp.read() 273 274 data = s = str(data, encoding='utf-8') 275 emit_result(stdout.buffer, eval_expr(expression, data)) 276 277 if len(args) == 0: 278 data = stdin.buffer.read() 279 data = s = str(data, encoding='utf-8') 280 emit_result(stdout.buffer, eval_expr(expression, data)) 281 except BrokenPipeError: 282 # quit quietly, instead of showing a confusing error message 283 stderr.close() 284 exit(0) 285 except KeyboardInterrupt: 286 exit(2) 287 except Exception as e: 288 s = str(e) 289 s = s if s else '<generic exception>' 290 print(s, file=stderr) 291 exit(1)