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 open_read = make_open_read(open)
 136 open = open_read
 137 
 138 exec = None
 139 
 140 
 141 def adapt_result(x, default):
 142     if x is True:
 143         return default
 144     if x is False:
 145         return None
 146 
 147     if isinstance(x, Skip):
 148         return None
 149 
 150     if callable(x):
 151         return x(default)
 152     return x
 153 
 154 
 155 def emit_result(w, x):
 156     if x is None:
 157         return
 158 
 159     if isinstance(x, (list, tuple, range, Generator)):
 160         for e in x:
 161             w.write(tobytes(e))
 162         return
 163 
 164     w.write(tobytes(x))
 165 
 166 
 167 def eval_expr(expr, using):
 168     global v, val, value, d, dat, data
 169     # offer several aliases for the variable with the input bytes
 170     v = val = value = d = dat = data = using
 171     return adapt_result(eval(expr), using)
 172 
 173 
 174 import functools
 175 import itertools
 176 import math
 177 import random
 178 import statistics
 179 import string
 180 import time
 181 
 182 
 183 try:
 184     if not expression or expression == '.':
 185         expression = 'data'
 186     expression = compile(expression, expression, 'eval')
 187 
 188     got_stdin = False
 189     all_stdin = None
 190     dashes = args.count('-')
 191 
 192     if any(seems_url(name) for name in args):
 193         from urllib.request import urlopen
 194 
 195     data = None
 196 
 197     if load_input:
 198         for name in args:
 199             if name == '-':
 200                 if dashes > 1:
 201                     if not got_stdin:
 202                         all_stdin = stdin.buffer.read()
 203                         got_stdin = True
 204                     data = all_stdin
 205                 else:
 206                     data = stdin.buffer.read()
 207             elif seems_url(name):
 208                 with urlopen(name) as inp:
 209                     data = inp.read()
 210             else:
 211                 with open_read(name) as inp:
 212                     data = inp.read()
 213 
 214             emit_result(stdout.buffer, eval_expr(expression, data))
 215 
 216         if len(args) == 0:
 217             data = stdin.buffer.read()
 218             emit_result(stdout.buffer, eval_expr(expression, data))
 219     else:
 220         emit_result(stdout.buffer, eval_expr(expression, None))
 221 except BrokenPipeError:
 222     # quit quietly, instead of showing a confusing error message
 223     stderr.close()
 224     exit(0)
 225 except KeyboardInterrupt:
 226     # stderr.close()
 227     exit(2)
 228 except Exception as e:
 229     s = str(e)
 230     s = s if s else '<generic exception>'
 231     print(f'\x1b[31m{s}\x1b[0m', file=stderr)
 232     exit(1)