File: minitl.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 minitl [options...] [python expression] [files/URIs...]
  28 
  29 This is the MINImal version of the Transform Lines tool.
  30 '''
  31 
  32 
  33 from json import dumps
  34 from sys import argv, exit, stderr, stdin
  35 from typing import Generator
  36 
  37 
  38 if len(argv) < 2:
  39     print(info.strip(), file=stderr)
  40     exit(0)
  41 if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'):
  42     print(info.strip())
  43     exit(0)
  44 
  45 
  46 def handle_no_input(expr):
  47     res = eval(expr)
  48     if isinstance(res, (list, range, tuple, Generator)):
  49         for e in res:
  50             if not isinstance(e, Skip):
  51                 print(e, flush=True)
  52         return
  53 
  54     res = adapt_result(res, None)
  55     if not (res is None):
  56         print(res, flush=True)
  57 
  58 
  59 def handle_lines(src, expr):
  60     # `comprehension` expressions seem to ignore local variables: even
  61     # lambda-based workarounds fail
  62     global i, l, line, v, val, value, e, err, error
  63 
  64     i = 0
  65     e = err = error = None
  66 
  67     for l in src:
  68         l = l.rstrip('\r\n').rstrip('\n')
  69         if i == 0:
  70             l = l.lstrip('\xef\xbb\xbf')
  71 
  72         line = l
  73         try:
  74             e = err = error = None
  75             v = val = value = loads(l)
  76         except Exception as ex:
  77             e = err = error = ex
  78             v = val = value = Skip()
  79         res = eval(expr)
  80         i += 1
  81 
  82         if isinstance(res, (list, range, tuple, Generator)):
  83             for e in res:
  84                 if not isinstance(e, Skip):
  85                     print(e, flush=True)
  86             continue
  87 
  88         res = adapt_result(res, line)
  89         if not (res is None):
  90             print(res, flush=True)
  91 
  92 
  93 def hold_lines(src, lines):
  94     for e in src:
  95         lines.append(e)
  96         yield e
  97 
  98 
  99 def adapt_result(res, fallback):
 100     if isinstance(res, BaseException):
 101         raise res
 102     if isinstance(res, Skip):
 103         return res
 104     if res is None or res is False:
 105         return None
 106     if callable(res):
 107         return res(fallback)
 108     if res is True:
 109         return fallback
 110     if isinstance(res, dict):
 111         return dumps(res, allow_nan=False)
 112     return str(res)
 113 
 114 
 115 class Skip:
 116     pass
 117 
 118 
 119 skip = Skip()
 120 
 121 
 122 def rescue(attempt, fallback = None):
 123     try:
 124         return attempt()
 125     except Exception as e:
 126         if callable(fallback):
 127             return fallback(e)
 128         return fallback
 129 
 130 catch = rescue
 131 catched = rescue
 132 caught = rescue
 133 recover = rescue
 134 recovered = rescue
 135 rescued = rescue
 136 
 137 
 138 def make_open_utf8(open):
 139     def open_utf8_readonly(path):
 140         return open(path, encoding='utf-8')
 141     return open_utf8_readonly
 142 
 143 
 144 def seems_url(path):
 145     protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:')
 146     return any(path.startswith(p) for p in protocols)
 147 
 148 
 149 cr = '\r'
 150 crlf = '\r\n'
 151 dquo = '"'
 152 dquote = '"'
 153 empty = ''
 154 lcurly = '{'
 155 lf = '\n'
 156 rcurly = '}'
 157 s = ''
 158 squo = '\''
 159 squote = '\''
 160 utf8bom = '\xef\xbb\xbf'
 161 
 162 nil = None
 163 none = None
 164 null = None
 165 
 166 
 167 exec = None
 168 open_utf8 = make_open_utf8(open)
 169 open = open_utf8
 170 
 171 no_input_opts = (
 172     '=', '--n', '-nil', '--nil', '-none', '--none', '-null', '--null',
 173 )
 174 more_modules_opts = ('-mm', '--mm', '-more', '--more')
 175 
 176 args = argv[1:]
 177 if any(seems_url(e) for e in args):
 178     from io import TextIOWrapper
 179     from urllib.request import urlopen
 180 
 181 no_input = False
 182 while len(args) > 0:
 183     if args[0] in no_input_opts:
 184         no_input = True
 185         args = args[1:]
 186         continue
 187 
 188     if args[0] in more_modules_opts:
 189         import functools
 190         import itertools
 191         import math
 192         import random
 193         import statistics
 194         import string
 195         import time
 196         args = args[1:]
 197         continue
 198 
 199     break
 200 
 201 
 202 try:
 203     expr = '.'
 204     if len(args) > 0:
 205         expr = args[0]
 206         args = args[1:]
 207 
 208     # if expr in globals().keys():
 209     #     v = globals().get(expr)
 210     #     if callable(v):
 211     #         expr = f'{expr}(line)'
 212 
 213     if expr == '.' and no_input:
 214         print(info.strip(), file=stderr)
 215         exit(0)
 216 
 217     if expr == '.':
 218         expr = 'line'
 219     expr = compile(expr, expr, 'eval')
 220 
 221     if no_input:
 222         handle_no_input(expr)
 223         exit(0)
 224 
 225     if len(args) == 0:
 226         handle_lines(stdin, expr)
 227         exit(0)
 228 
 229     got_stdin = False
 230     all_stdin = None
 231     dashes = args.count('-')
 232 
 233     for path in args:
 234         if path == '-':
 235             if dashes > 1:
 236                 if not got_stdin:
 237                     handle_lines(hold_lines(stdin, all_stdin), expr)
 238                     got_stdin = True
 239                 else:
 240                     handle_lines(all_stdin, expr)
 241             else:
 242                 handle_lines(stdin, expr)
 243             continue
 244 
 245         if seems_url(path):
 246             with urlopen(path) as inp:
 247                 with TextIOWrapper(inp, encoding='utf-8') as txt:
 248                     handle_lines(txt, expr)
 249             continue
 250 
 251         with open_utf8(path) as txt:
 252             handle_lines(txt, expr)
 253 except BrokenPipeError:
 254     # quit quietly, instead of showing a confusing error message
 255     stderr.close()
 256     exit(0)
 257 except KeyboardInterrupt:
 258     # stderr.close()
 259     exit(2)
 260 except Exception as e:
 261     print(f'\x1b[31m{str(e)}\x1b[0m', file=stderr)
 262     exit(1)