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