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 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 isinstance(x, (list, tuple, range, Generator)): 131 for e in x: 132 emit_simple_value(w, e) 133 else: 134 emit_simple_value(w, x) 135 136 def emit_simple_value(w, x): 137 if x is None: 138 return 139 if isinstance(x, int): 140 x = str(x) 141 w.write(bytes(x, encoding='utf-8')) 142 143 def eval_expr(expr, using): 144 global v, val, value, d, dat, data 145 # offer several aliases for the variable with the input string 146 s = v = val = value = d = dat = data = using 147 return adapt_result(eval(expr), using) 148 149 150 nil = none = null = None 151 152 153 exec = None 154 open = make_open_read(open) 155 156 no_input_opts = ( 157 '=', '-n', '--n', '-nil', '--nil', '-none', '--none', '-null', '--null', 158 ) 159 modules_opts = ( 160 '-m', '--m', '-mod', '--mod', '-module', '--module', 161 '-modules', '--modules', 162 ) 163 more_modules_opts = ('-mm', '--mm', '-more', '--more') 164 165 args = argv[1:] 166 load_input = True 167 expression = None 168 169 while len(args) > 0: 170 if args[0] == '--': 171 args = args[1:] 172 break 173 174 if args[0] in no_input_opts: 175 load_input = False 176 args = args[1:] 177 continue 178 179 if args[0] in no_input_opts: 180 no_input = True 181 args = args[1:] 182 continue 183 184 if args[0] in modules_opts: 185 try: 186 if len(args) < 2: 187 msg = 'a module name or a comma-separated list of modules' 188 raise Exception('expected ' + msg) 189 190 g = globals() 191 from importlib import import_module 192 for e in args[1].split(','): 193 g[e] = import_module(e) 194 195 g = None 196 import_module = None 197 args = args[2:] 198 except Exception as e: 199 fail(e, 1) 200 201 continue 202 203 if args[0] in more_modules_opts: 204 import functools, itertools, json, math, random, statistics, string, time 205 args = args[1:] 206 continue 207 208 break 209 210 cr = '\r' 211 crlf = '\r\n' 212 dquo = dquote = '"' 213 empty = '' 214 lcurly = '{' 215 lf = '\n' 216 rcurly = '}' 217 space = ' ' 218 squo = squote = '\'' 219 tab = '\t' 220 utf8bom = '\xef\xbb\xbf' 221 bom = { 222 'utf8': '\xef\xbb\xbf', 223 'utf16be': '\xfe\xff', 224 'utf16le': '\xff\xfe', 225 'utf32be': '\x00\x00\xfe\xff', 226 'utf32le': '\xff\xfe\x00\x00', 227 } 228 229 if len(args) > 0: 230 expression = args[0] 231 args = args[1:] 232 233 if expression is None: 234 print(info.strip(), file=stderr) 235 exit(0) 236 237 try: 238 if not expression or expression == '.': 239 expression = 'data' 240 expression = compile(expression, expression, 'eval') 241 242 got_stdin = False 243 all_stdin = None 244 dashes = args.count('-') 245 246 data = None 247 248 if not load_input: 249 emit_result(stdout.buffer, eval_expr(expression, None)) 250 exit(0) 251 252 if any(seemsurl(name) for name in args): 253 from urllib.request import urlopen 254 255 for name in args: 256 if name == '-': 257 if dashes > 1: 258 if not got_stdin: 259 all_stdin = stdin.buffer.read() 260 got_stdin = True 261 data = all_stdin 262 else: 263 data = stdin.buffer.read() 264 265 data = s = str(data, encoding='utf-8') 266 elif seemsurl(name): 267 with urlopen(name) as inp: 268 data = inp.read() 269 else: 270 with open(name) as inp: 271 data = inp.read() 272 273 data = s = str(data, encoding='utf-8') 274 emit_result(stdout.buffer, eval_expr(expression, data)) 275 276 if len(args) == 0: 277 data = stdin.buffer.read() 278 data = s = str(data, encoding='utf-8') 279 emit_result(stdout.buffer, eval_expr(expression, data)) 280 except BrokenPipeError: 281 # quit quietly, instead of showing a confusing error message 282 stderr.close() 283 exit(0) 284 except KeyboardInterrupt: 285 exit(2) 286 except Exception as e: 287 s = str(e) 288 s = s if s else '<generic exception>' 289 print(s, file=stderr) 290 exit(1)