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