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)