File: minitj.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 minitj [options...] [python expression] [files/URIs...] 28 29 This is the MINImal version of the Transform Json tool. 30 ''' 31 32 33 from io import TextIOWrapper 34 from json import dump, load 35 from sys import argv, exit, stderr, stdin, stdout 36 from typing import Iterable 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 class Skip: 45 pass 46 47 48 skip = Skip() 49 50 51 def rescue(attempt, fallback = None): 52 try: 53 return attempt() 54 except Exception as e: 55 if callable(fallback): 56 return fallback(e) 57 return fallback 58 59 catch = rescue 60 catched = rescue 61 caught = rescue 62 recover = rescue 63 recovered = rescue 64 rescued = rescue 65 66 67 def result_needs_fixing(x): 68 if x is None or isinstance(x, (bool, int, float, str)): 69 return False 70 rec = result_needs_fixing 71 if isinstance(x, dict): 72 return any(rec(k) or rec(v) for k, v in x.items()) 73 if isinstance(x, (list, tuple)): 74 return any(rec(e) for e in x) 75 return True 76 77 78 def fix_result(x, default): 79 if x is type: 80 return type(default).__name__ 81 82 # if expression results in a func, auto-call it with the original data 83 if callable(x): 84 x = x(default) 85 86 if x is None or isinstance(x, (bool, int, float, str)): 87 return x 88 89 rec = fix_result 90 91 if isinstance(x, dict): 92 return { 93 rec(k, default): rec(v, default) for k, v in x.items() if not 94 (isinstance(k, Skip) or isinstance(v, Skip)) 95 } 96 if isinstance(x, Iterable): 97 return tuple(rec(e, default) for e in x if not isinstance(e, Skip)) 98 99 if isinstance(x, Exception): 100 raise x 101 102 return None if isinstance(x, Skip) else str(x) 103 104 105 def seems_url(path): 106 protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') 107 return any(path.startswith(p) for p in protocols) 108 109 110 cr = '\r' 111 crlf = '\r\n' 112 dquo = '"' 113 dquote = '"' 114 empty = '' 115 lcurly = '{' 116 lf = '\n' 117 rcurly = '}' 118 s = '' 119 squo = '\'' 120 squote = '\'' 121 utf8bom = '\xef\xbb\xbf' 122 123 nil = None 124 none = None 125 null = None 126 127 128 no_input_opts = ( 129 '=', '-n', '--n', '-nil', '--nil', '-none', '--none', '-null', '--null', 130 ) 131 compact_output_opts = ( 132 '-c', '--c', '-compact', '--compact', '-j0', '--j0', '-json0', '--json0', 133 ) 134 more_modules_opts = ('-mm', '--mm', '-more', '--more') 135 136 args = argv[1:] 137 no_input = False 138 compact_output = False 139 140 while len(args) > 0: 141 if args[0] in no_input_opts: 142 no_input = True 143 args = args[1:] 144 continue 145 146 if args[0] in compact_output_opts: 147 compact_output = True 148 args = args[1:] 149 continue 150 151 if args[0] in more_modules_opts: 152 import functools 153 import itertools 154 import math 155 import random 156 import statistics 157 import string 158 import time 159 args = args[1:] 160 continue 161 162 break 163 164 165 try: 166 expr = 'data' 167 if len(args) > 0: 168 expr = args[0] 169 args = args[1:] 170 171 if expr == '.': 172 expr = 'data' 173 174 if len(args) > 1: 175 raise Exception('can\'t use more than 1 input') 176 path = '-' if len(args) == 0 else args[0] 177 178 if no_input: 179 data = None 180 elif path == '-': 181 data = load(stdin) 182 elif seems_url(path): 183 from urllib.request import urlopen 184 with urlopen(path) as inp: 185 with TextIOWrapper(inp, encoding='utf-8') as txt: 186 data = load(txt) 187 else: 188 with open(path, encoding='utf-8') as inp: 189 data = load(inp) 190 191 exec = None 192 open = None 193 v = value = d = data 194 v = eval(expr) 195 if result_needs_fixing(v): 196 v = fix_result(v, data) 197 198 if compact_output: 199 dump(v, stdout, indent=None, separators=(',', ':'), allow_nan=False) 200 else: 201 dump(v, stdout, indent=2, separators=(',', ': '), allow_nan=False) 202 print() 203 except BrokenPipeError: 204 exit(0) 205 except Exception as e: 206 print(f'\x1b[31m{str(e)}\x1b[0m', file=stderr) 207 exit(1)