File: tbp.py 1 #!/usr/bin/python3 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2024 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 tbp [options...] [python expression] [files/URIs...] 28 29 30 Transform Bytes with Python runs a python expression on each whole-input, 31 read as a bytes-type value. 32 33 The expression can use either `v`, `value`, `d`, or `data` for the current 34 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 typing import Generator 49 50 51 if len(argv) < 2: 52 print(info.strip(), file=stderr) 53 exit(0) 54 if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): 55 print(info.strip()) 56 exit(0) 57 58 59 class Skip: 60 pass 61 62 63 skip = Skip() 64 65 66 def cond(*args): 67 if len(args) == 0: 68 return None 69 70 for i, e in enumerate(args): 71 if i % 2 == 0 and i < len(args) - 1 and e: 72 return args[i + 1] 73 74 return args[-1] if len(args) % 2 == 1 else None 75 76 77 def rescue(attempt, fallback = None): 78 try: 79 return attempt() 80 except Exception as e: 81 if callable(fallback): 82 return fallback(e) 83 return fallback 84 85 catch = rescue 86 catched = rescue 87 caught = rescue 88 recover = rescue 89 recovered = rescue 90 rescued = rescue 91 92 93 def wait(seconds, result): 94 t = (int, float) 95 if (not isinstance(seconds, t)) and isinstance(result, t): 96 seconds, result = result, seconds 97 sleep(seconds) 98 return result 99 100 delay = wait 101 102 103 no_input_opts = ( 104 '=', '-n', '--n', '-nil', '--nil', '-none', '--none', '-null', '--null', 105 ) 106 string_opts = ('-s', '--s', '-str', '--str', '-string', '--string') 107 more_modules_opts = ('-mm', '--mm', '-more', '--more') 108 109 args = argv[1:] 110 load_input = True 111 string_input = False 112 expression = None 113 114 # handle all other leading options; the explicit help options are 115 # handled earlier in the script 116 while len(args) > 0: 117 if args[0] in no_input_opts: 118 load_input = False 119 args = args[1:] 120 continue 121 122 if args[0] in more_modules_opts: 123 import functools 124 import itertools 125 import json 126 import math 127 import random 128 import statistics 129 import string 130 import time 131 args = args[1:] 132 continue 133 134 if args[0] in string_opts: 135 string_input = True 136 args = args[1:] 137 continue 138 139 break 140 141 if len(args) > 0: 142 expression = args[0] 143 args = args[1:] 144 145 if expression is None: 146 print(info.strip(), file=stderr) 147 exit(0) 148 149 150 def make_open_read(open): 151 'Restrict the file-open func to a read-only-binary file-open func.' 152 def open_read(name): 153 return open(name, mode='rb') 154 return open_read 155 156 157 def fail(msg, code = 1): 158 print(f'\x1b[31m{str(msg)}\x1b[0m', file=stderr) 159 exit(code) 160 161 162 def message(msg, result = None): 163 print(msg, file=stderr) 164 return result 165 166 msg = message 167 168 169 def seemsurl(s): 170 protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') 171 return any(s.startswith(p) for p in protocols) 172 173 174 def tobytes(x): 175 if isinstance(x, (bytearray, bytes)): 176 return x 177 if isinstance(x, (bool, int)): 178 return bytes((int(x), )) 179 if isinstance(x, float): 180 return bytes(str(x), encoding='utf-8') 181 if isinstance(x, str): 182 return bytes(x, encoding='utf-8') 183 return bytes(x) 184 185 186 def tointorbytes(x): 187 return x if isinstance(x, int) else tobytes(x) 188 189 190 def adapt_result(x, default): 191 if x is True: 192 return default 193 if x is False: 194 return None 195 196 if isinstance(x, Skip): 197 return None 198 199 if callable(x): 200 return x(default) 201 return x 202 203 204 def emit_result(w, x): 205 if x is None: 206 return 207 208 if isinstance(x, int): 209 w.write(tobytes(x)) 210 return 211 212 if isinstance(x, (list, tuple, range, Generator)): 213 for e in x: 214 w.write(tobytes(e)) 215 return 216 217 w.write(tobytes(x)) 218 219 220 def eval_expr(expr, using): 221 global v, val, value, d, dat, data 222 # offer several aliases for the variable with the input bytes 223 v = val = value = d = dat = data = using 224 return adapt_result(eval(expr), using) 225 226 227 cr = '\r' if string_input else b'\r' 228 crlf = '\r\n' if string_input else b'\r\n' 229 dquo = '"' if string_input else b'"' 230 dquote = '"' if string_input else b'"' 231 empty = '' if string_input else b'' 232 lcurly = '{' if string_input else b'{' 233 lf = '\n' if string_input else b'\n' 234 rcurly = '}' if string_input else b'}' 235 s = '' if string_input else b'' 236 squo = '\'' if string_input else b'\'' 237 squote = '\'' if string_input else b'\'' 238 utf8bom = '\xef\xbb\xbf' if string_input else b'\xef\xbb\xbf' 239 240 nil = None 241 none = None 242 null = None 243 244 exec = None 245 open = make_open_read(open) 246 247 modules_opts = ( 248 '-m', '--m', '-mod', '--mod', '-module', '--module', 249 '-modules', '--modules', 250 ) 251 more_modules_opts = ('-mm', '--mm', '-more', '--more') 252 253 while len(args) > 0: 254 if args[0] in no_input_opts: 255 no_input = True 256 args = args[1:] 257 continue 258 259 if args[0] in modules_opts: 260 try: 261 if len(args) < 2: 262 msg = 'a module name or a comma-separated list of modules' 263 raise Exception('expected ' + msg) 264 265 g = globals() 266 from importlib import import_module 267 for e in args[1].split(','): 268 g[e] = import_module(e) 269 270 g = None 271 import_module = None 272 args = args[2:] 273 except Exception as e: 274 fail(e, 1) 275 276 continue 277 278 if args[0] in more_modules_opts: 279 import functools, itertools, json, math, random, statistics, string, time 280 args = args[1:] 281 continue 282 283 break 284 285 286 try: 287 if not expression or expression == '.': 288 expression = 'data' 289 expression = compile(expression, expression, 'eval') 290 291 got_stdin = False 292 all_stdin = None 293 dashes = args.count('-') 294 295 data = None 296 297 if not load_input: 298 emit_result(stdout.buffer, eval_expr(expression, None)) 299 exit(0) 300 301 if any(seemsurl(name) for name in args): 302 from urllib.request import urlopen 303 304 for name in args: 305 if name == '-': 306 if dashes > 1: 307 if not got_stdin: 308 all_stdin = stdin.buffer.read() 309 got_stdin = True 310 data = all_stdin 311 else: 312 data = stdin.buffer.read() 313 314 if string_input: 315 data = str(data, encoding='utf-8') 316 elif seemsurl(name): 317 with urlopen(name) as inp: 318 data = inp.read() 319 else: 320 with open(name) as inp: 321 data = inp.read() 322 323 if string_input and not isinstance(data, str): 324 data = str(data, encoding='utf-8') 325 emit_result(stdout.buffer, eval_expr(expression, data)) 326 327 if len(args) == 0: 328 data = stdin.buffer.read() 329 if string_input and not isinstance(data, str): 330 data = str(data, encoding='utf-8') 331 emit_result(stdout.buffer, eval_expr(expression, data)) 332 except BrokenPipeError: 333 # quit quietly, instead of showing a confusing error message 334 stderr.close() 335 exit(0) 336 except KeyboardInterrupt: 337 # stderr.close() 338 exit(2) 339 except Exception as e: 340 s = str(e) 341 s = s if s else '<generic exception>' 342 print(f'\x1b[31m{s}\x1b[0m', file=stderr) 343 exit(1)