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)