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