File: minitb.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 tb [options...] [python expression] [filepath/URI...] 28 29 This is the MINImal version of the Transform Bytes tool. 30 ''' 31 32 33 from sys import argv, exit, stderr, stdin, stdout 34 from typing import Generator 35 36 37 # no args or a leading help-option arg means show the help message and quit 38 help_opts = ('-h', '--h', '-help', '--help') 39 if len(argv) < 2 or (len(argv) == 2 and argv[1] in help_opts): 40 print(info.strip(), file=stderr) 41 exit(0) 42 43 44 class Skip: 45 pass 46 47 48 skip = Skip() 49 50 51 def rescue(attempt, fallback = None): 52 try: 53 return attempt() 54 except Exception as e: 55 if callable(fallback): 56 return fallback(e) 57 return fallback 58 59 catch = rescue 60 catched = rescue 61 caught = rescue 62 recover = rescue 63 recovered = rescue 64 rescued = rescue 65 66 67 no_input_opts = ( 68 '=', '-n', '--n', '-nil', '--nil', '-none', '--none', '-null', '--null', 69 ) 70 more_modules_opts = ('-mm', '--mm', '-more', '--more') 71 72 args = argv[1:] 73 load_input = True 74 expression = None 75 76 # handle all other leading options; the explicit help options are 77 # handled earlier in the script 78 while len(args) > 0: 79 if args[0] in no_input_opts: 80 load_input = False 81 args = args[1:] 82 continue 83 84 if args[0] in more_modules_opts: 85 import functools 86 import itertools 87 import math 88 import random 89 import statistics 90 import string 91 import time 92 args = args[1:] 93 continue 94 95 break 96 97 if len(args) > 0: 98 expression = args[0] 99 args = args[1:] 100 101 if expression is None: 102 print(info.strip(), file=stderr) 103 exit(0) 104 105 106 def make_open_read(open): 107 'Restrict the file-open func to a read-only-binary file-open func.' 108 def open_readonly(name): 109 return open(name, mode='rb') 110 return open_readonly 111 112 113 def seems_url(s): 114 protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') 115 return any(s.startswith(p) for p in protocols) 116 117 118 def tobytes(x): 119 if isinstance(x, bytes): 120 return x 121 if isinstance(x, (bool, int)): 122 return bytes(int(x)) 123 if isinstance(x, float): 124 return bytes(str(x), encoding='utf-8') 125 if isinstance(x, str): 126 return bytes(x, encoding='utf-8') 127 return bytes(x) 128 129 130 def tointorbytes(x): 131 return x if isinstance(x, int) else tobytes(x) 132 133 134 open_read = make_open_read(open) 135 open = open_read 136 137 exec = None 138 139 140 def adapt_result(x, default): 141 if x is True: 142 return default 143 if x is False: 144 return None 145 146 if isinstance(x, Skip): 147 return None 148 149 if callable(x): 150 return x(default) 151 return x 152 153 154 def emit_result(w, x): 155 if x is None: 156 return 157 158 if isinstance(x, (list, tuple, range, Generator)): 159 for e in x: 160 w.write(tobytes(e)) 161 return 162 163 w.write(tobytes(x)) 164 165 166 def eval_expr(expr, using): 167 global v, val, value, d, dat, data 168 # offer several aliases for the variable with the input bytes 169 v = val = value = d = dat = data = using 170 return adapt_result(eval(expr), using) 171 172 173 import functools 174 import itertools 175 import math 176 import random 177 import statistics 178 import string 179 import time 180 181 182 try: 183 if not expression or expression == '.': 184 expression = 'data' 185 expression = compile(expression, expression, 'eval') 186 187 got_stdin = False 188 all_stdin = None 189 dashes = args.count('-') 190 191 if any(seems_url(name) for name in args): 192 from urllib.request import urlopen 193 194 data = None 195 196 if load_input: 197 for name in args: 198 if name == '-': 199 if dashes > 1: 200 if not got_stdin: 201 all_stdin = stdin.buffer.read() 202 got_stdin = True 203 data = all_stdin 204 else: 205 data = stdin.buffer.read() 206 elif seems_url(name): 207 with urlopen(name) as inp: 208 data = inp.read() 209 else: 210 with open_read(name) as inp: 211 data = inp.read() 212 213 emit_result(stdout.buffer, eval_expr(expression, data)) 214 215 if len(args) == 0: 216 data = stdin.buffer.read() 217 emit_result(stdout.buffer, eval_expr(expression, data)) 218 else: 219 emit_result(stdout.buffer, eval_expr(expression, None)) 220 except BrokenPipeError: 221 # quit quietly, instead of showing a confusing error message 222 stderr.close() 223 except KeyboardInterrupt: 224 exit(2) 225 except Exception as e: 226 s = str(e) 227 s = s if s else '<generic exception>' 228 print(f'\x1b[31m{s}\x1b[0m', file=stderr) 229 exit(1)