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 space = ' ' if string_input else b' ' 236 squo = '\'' if string_input else b'\'' 237 squote = '\'' if string_input else b'\'' 238 tab = '\t' if string_input else b'\t' 239 utf8bom = '\xef\xbb\xbf' if string_input else b'\xef\xbb\xbf' 240 241 nil = None 242 none = None 243 null = None 244 245 exec = None 246 open = make_open_read(open) 247 248 modules_opts = ( 249 '-m', '--m', '-mod', '--mod', '-module', '--module', 250 '-modules', '--modules', 251 ) 252 more_modules_opts = ('-mm', '--mm', '-more', '--more') 253 254 while len(args) > 0: 255 if args[0] in no_input_opts: 256 no_input = True 257 args = args[1:] 258 continue 259 260 if args[0] in modules_opts: 261 try: 262 if len(args) < 2: 263 msg = 'a module name or a comma-separated list of modules' 264 raise Exception('expected ' + msg) 265 266 g = globals() 267 from importlib import import_module 268 for e in args[1].split(','): 269 g[e] = import_module(e) 270 271 g = None 272 import_module = None 273 args = args[2:] 274 except Exception as e: 275 fail(e, 1) 276 277 continue 278 279 if args[0] in more_modules_opts: 280 import functools, itertools, json, math, random, statistics, string, time 281 args = args[1:] 282 continue 283 284 break 285 286 287 try: 288 if not expression or expression == '.': 289 expression = 'data' 290 expression = compile(expression, expression, 'eval') 291 292 got_stdin = False 293 all_stdin = None 294 dashes = args.count('-') 295 296 data = None 297 298 if not load_input: 299 emit_result(stdout.buffer, eval_expr(expression, None)) 300 exit(0) 301 302 if any(seemsurl(name) for name in args): 303 from urllib.request import urlopen 304 305 for name in args: 306 if name == '-': 307 if dashes > 1: 308 if not got_stdin: 309 all_stdin = stdin.buffer.read() 310 got_stdin = True 311 data = all_stdin 312 else: 313 data = stdin.buffer.read() 314 315 if string_input: 316 data = str(data, encoding='utf-8') 317 elif seemsurl(name): 318 with urlopen(name) as inp: 319 data = inp.read() 320 else: 321 with open(name) as inp: 322 data = inp.read() 323 324 if string_input and not isinstance(data, str): 325 data = str(data, encoding='utf-8') 326 emit_result(stdout.buffer, eval_expr(expression, data)) 327 328 if len(args) == 0: 329 data = stdin.buffer.read() 330 if string_input and not isinstance(data, str): 331 data = str(data, encoding='utf-8') 332 emit_result(stdout.buffer, eval_expr(expression, data)) 333 except BrokenPipeError: 334 # quit quietly, instead of showing a confusing error message 335 stderr.close() 336 exit(0) 337 except KeyboardInterrupt: 338 # stderr.close() 339 exit(2) 340 except Exception as e: 341 s = str(e) 342 s = s if s else '<generic exception>' 343 print(f'\x1b[31m{s}\x1b[0m', file=stderr) 344 exit(1)