#!/usr/bin/python3 # The MIT License (MIT) # # Copyright © 2020-2025 pacman64 # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. info = ''' minitl [options...] [python expression] [files/URIs...] This is the MINImal version of the Transform Lines tool. ''' from json import dumps, loads from sys import argv, exit, stderr, stdin from typing import Generator if len(argv) < 2: print(info.strip(), file=stderr) exit(0) if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): print(info.strip()) exit(0) def handle_no_input(expr): res = eval(expr) if isinstance(res, (list, range, tuple, Generator)): for e in res: if not isinstance(e, Skip): print(e, flush=True) return res = adapt_result(res, None) if not (res is None): print(res, flush=True) def handle_lines(src, expr): # `comprehension` expressions seem to ignore local variables: even # lambda-based workarounds fail global i, l, line, v, val, value, e, err, error i = 0 e = err = error = None for l in src: l = l.rstrip('\r\n').rstrip('\n') if i == 0: l = l.lstrip('\xef\xbb\xbf') line = l try: e = err = error = None v = val = value = loads(l) except Exception as ex: e = err = error = ex v = val = value = Skip() res = eval(expr) i += 1 if isinstance(res, (list, range, tuple, Generator)): for e in res: if not isinstance(e, Skip): print(e, flush=True) continue res = adapt_result(res, line) if not (res is None): print(res, flush=True) def hold_lines(src, lines): for e in src: lines.append(e) yield e def adapt_result(res, fallback): if isinstance(res, BaseException): raise res if isinstance(res, Skip): return res if res is None or res is False: return None if callable(res): return res(fallback) if res is True: return fallback if isinstance(res, dict): return dumps(res, allow_nan=False) return str(res) class Skip: pass skip = Skip() def rescue(attempt, fallback = None): try: return attempt() except Exception as e: if callable(fallback): return fallback(e) return fallback catch = rescue catched = rescue caught = rescue recover = rescue recovered = rescue rescued = rescue def make_open_utf8(open): def open_utf8_readonly(path): return open(path, encoding='utf-8') return open_utf8_readonly def seems_url(path): protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') return any(path.startswith(p) for p in protocols) cr = '\r' crlf = '\r\n' dquo = '"' dquote = '"' empty = '' lcurly = '{' lf = '\n' rcurly = '}' s = '' squo = '\'' squote = '\'' utf8bom = '\xef\xbb\xbf' nil = None none = None null = None exec = None open_utf8 = make_open_utf8(open) open = open_utf8 no_input_opts = ( '=', '--n', '-nil', '--nil', '-none', '--none', '-null', '--null', ) more_modules_opts = ('-mm', '--mm', '-more', '--more') args = argv[1:] if any(seems_url(e) for e in args): from io import TextIOWrapper from urllib.request import urlopen no_input = False while len(args) > 0: if args[0] in no_input_opts: no_input = True args = args[1:] continue if args[0] in more_modules_opts: import functools import itertools import math import random import statistics import string import time args = args[1:] continue break try: expr = '.' if len(args) > 0: expr = args[0] args = args[1:] # if expr in globals().keys(): # v = globals().get(expr) # if callable(v): # expr = f'{expr}(line)' if expr == '.' and no_input: print(info.strip(), file=stderr) exit(0) if expr == '.': expr = 'line' expr = compile(expr, expr, 'eval') if no_input: handle_no_input(expr) exit(0) if len(args) == 0: handle_lines(stdin, expr) exit(0) got_stdin = False all_stdin = None dashes = args.count('-') for path in args: if path == '-': if dashes > 1: if not got_stdin: handle_lines(hold_lines(stdin, all_stdin), expr) got_stdin = True else: handle_lines(all_stdin, expr) else: handle_lines(stdin, expr) continue if seems_url(path): with urlopen(path) as inp: with TextIOWrapper(inp, encoding='utf-8') as txt: handle_lines(txt, expr) continue with open_utf8(path) as txt: handle_lines(txt, expr) except BrokenPipeError: # quit quietly, instead of showing a confusing error message stderr.close() exit(0) except KeyboardInterrupt: exit(2) except Exception as e: print(f'\x1b[31m{str(e)}\x1b[0m', file=stderr) exit(1)