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 io import TextIOWrapper 34 from json import dumps 35 from sys import argv, exit, stderr, stdin 36 from typing import Generator 37 38 39 if len(argv) < 2 or argv[1] in ('-h', '--h', '-help', '--help'): 40 print(info.strip(), file=stderr) 41 exit(0) 42 43 44 def handle_no_input(expr): 45 res = eval(expr) 46 if isinstance(res, (list, range, tuple, Generator)): 47 for e in res: 48 if not isinstance(e, Skip): 49 print(e) 50 return 51 52 res = adapt_result(res, None) 53 if not (res is None): 54 print(res) 55 56 57 def handle_lines(src, expr): 58 # `comprehension` expressions seem to ignore local variables: even 59 # lambda-based workarounds fail 60 global i, l, line 61 62 i = 0 63 for e in src: 64 l = e.rstrip('\r\n').rstrip('\n') 65 if i == 0: 66 l = l.lstrip('\xef\xbb\xbf') 67 68 line = l 69 res = eval(expr) 70 i += 1 71 72 if isinstance(res, (list, range, tuple, Generator)): 73 for e in res: 74 if not isinstance(e, Skip): 75 print(e) 76 continue 77 78 res = adapt_result(res, line) 79 if not (res is None): 80 print(res) 81 82 83 def adapt_result(res, fallback): 84 if callable(res): 85 return res(fallback) 86 87 if res is None or res is False or isinstance(res, Skip): 88 return None 89 if res is True: 90 return fallback 91 if isinstance(res, dict): 92 return dumps(res, allow_nan=False) 93 return str(res) 94 95 96 class Skip: 97 pass 98 99 100 skip = Skip() 101 102 103 def recover(attempt, fallback = None): 104 try: 105 return attempt() 106 except Exception as e: 107 if callable(fallback): 108 return fallback(e) 109 return fallback 110 111 recovered = recover 112 rescue = recover 113 rescued = recover 114 115 116 def make_open_utf8(open): 117 def open_utf8_readonly(path): 118 return open(path, encoding='utf-8') 119 return open_utf8_readonly 120 121 122 def seems_url(path): 123 protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') 124 return any(path.startswith(p) for p in protocols) 125 126 127 crlf = '\r\n' 128 dquo = '"' 129 dquote = '"' 130 empty = '' 131 lcurly = '{' 132 lf = '\n' 133 rcurly = '}' 134 s = '' 135 squo = '\'' 136 squote = '\'' 137 138 nil = None 139 none = None 140 null = None 141 142 143 exec = None 144 open_utf8 = make_open_utf8(open) 145 open = open_utf8 146 147 args = argv[1:] 148 if any(seems_url(e) for e in args): 149 from urllib.request import urlopen 150 151 no_input_opts = ( 152 '=', '--n', '-nil', '--nil', '-none', '--none', '-null', '--null', 153 ) 154 155 no_input = False 156 while len(args) > 0: 157 if args[0] in no_input_opts: 158 no_input = True 159 args = args[1:] 160 continue 161 break 162 163 164 import functools 165 import itertools 166 import math 167 import random 168 import statistics 169 import string 170 import time 171 172 173 try: 174 expr = '.' 175 if len(args) > 0: 176 expr = args[0] 177 args = args[1:] 178 179 if expr == '.' and no_input: 180 print(info.strip(), file=stderr) 181 exit(0) 182 183 if expr == '.': 184 expr = 'line' 185 expr = compile(expr, expr, 'eval') 186 187 if no_input: 188 handle_no_input(expr) 189 exit(0) 190 191 if len(args) == 0: 192 handle_lines(stdin, expr) 193 exit(0) 194 195 got_stdin = False 196 all_stdin = None 197 dashes = args.count('-') 198 199 for path in args: 200 if path == '-': 201 if dashes > 1: 202 if not got_stdin: 203 all_stdin = stdin.read().splitlines() 204 got_stdin = True 205 handle_lines(all_stdin, expr) 206 else: 207 handle_lines(stdin, expr) 208 continue 209 210 if seems_url(path): 211 with urlopen(path) as inp: 212 with TextIOWrapper(inp, encoding='utf-8') as txt: 213 handle_lines(txt, expr) 214 continue 215 216 with open_utf8(path) as txt: 217 handle_lines(txt, expr) 218 except BrokenPipeError: 219 exit(0) 220 except Exception as e: 221 print(f'\x1b[31m{str(e)}\x1b[0m', file=stderr) 222 exit(1)