File: nj.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 from json import dumps, load 27 from math import modf 28 from sys import argv, exit, stderr, stdin, stdout 29 30 31 info = ''' 32 nj [filepath/URI...] 33 34 35 Nice Json indents and styles JSON data. When not given a filepath to read 36 data from, this tool reads from standard input. 37 ''' 38 39 if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): 40 print(info.strip(), file=stderr) 41 exit(0) 42 43 44 # level2indent speeds up func indent 45 level2indent = tuple(i * ' ' for i in range(64)) 46 47 48 def indent(w, level: int) -> None: 49 'Emit 2 spaces for each indentation level.' 50 51 if level < 1: 52 return 53 54 if level < len(level2indent): 55 w.write(level2indent[level]) 56 return 57 58 while level >= len(level2indent): 59 w.write(level2indent[-1]) 60 level -= len(level2indent) 61 w.write(level2indent[level]) 62 63 64 def restyle(w, data, pre: int = 0, level: int = 0) -> None: 65 'This func is where all the recursive output-action starts/happens.' 66 67 f = type2func[type(data)] 68 if f != None: 69 f(w, data, pre, level) 70 # don't forget to end the final line, if at `top-level` 71 if level == 0: 72 w.write('\n') 73 else: 74 raise ValueError(f'unsupported type {type(data)}') 75 76 77 def restyle_bool(w, data: bool, pre: int, level: int) -> None: 78 indent(w, pre) 79 if data: 80 w.write('\x1b[38;5;74mtrue\x1b[0m') 81 else: 82 w.write('\x1b[38;5;74mfalse\x1b[0m') 83 84 85 def restyle_dict(w, data: dict, pre: int, level: int) -> None: 86 if len(data) == 0: 87 indent(w, pre) 88 w.write('\x1b[38;5;248m{}\x1b[0m') 89 return 90 91 indent(w, pre) 92 w.write('\x1b[38;5;248m{\x1b[0m\n') 93 for (i, k) in enumerate(data): 94 if i > 0: 95 w.write('\x1b[38;5;248m,\x1b[0m\n') 96 restyle_key(w, k, level + 1) 97 restyle(w, data[k], 0, level + 1) 98 w.write('\n') 99 indent(w, level) 100 w.write('\x1b[38;5;248m}\x1b[0m') 101 102 103 def restyle_float(w, data: float, pre: int, level: int) -> None: 104 indent(w, pre) 105 # can't figure out a direct float-format option which both avoids the 106 # chance of scientific notation (invalid as JSON), and the chance of 107 # unneeded trailing decimal 0s; neither {data:-1} nor {data:-1f} work 108 if modf(data)[0] == 0.0: 109 w.write(f'\x1b[38;5;29m{data}\x1b[0m') 110 else: 111 s = f'{data:-1f}'.rstrip('0') 112 w.write(f'\x1b[38;5;29m{s}\x1b[0m') 113 114 115 def restyle_int(w, data: int, pre: int, level: int) -> None: 116 indent(w, pre) 117 w.write(f'\x1b[38;5;29m{data}\x1b[0m') 118 119 120 def restyle_key(w, k: str, level: int) -> None: 121 indent(w, level) 122 f = dumps 123 w.write(f'\x1b[38;5;248m"\x1b[38;5;99m{f(k)[1:-1]}\x1b[38;5;248m": \x1b[0m') 124 125 126 def restyle_list(w, data: list, pre: int, level: int) -> None: 127 if len(data) == 0: 128 indent(w, pre) 129 w.write('\x1b[38;5;248m[]\x1b[0m') 130 return 131 132 indent(w, pre) 133 w.write('\x1b[38;5;248m[\x1b[0m\n') 134 for (i, e) in enumerate(data): 135 if i > 0: 136 w.write('\x1b[38;5;248m,\x1b[0m\n') 137 restyle(w, e, level + 1, level + 1) 138 w.write('\n') 139 indent(w, level) 140 w.write('\x1b[38;5;248m]\x1b[0m') 141 142 143 def restyle_none(w, data: bool, pre: int, level: int) -> None: 144 indent(w, pre) 145 w.write('\x1b[38;5;248mnull\x1b[0m') 146 147 148 def restyle_str(w, data: str, pre: int, level: int) -> None: 149 indent(w, pre) 150 # f'\x1b[38;5;248m"\x1b[38;5;24m{dumps(data)[1:-1]}\x1b[38;5;248m"\x1b[0m' 151 w.write(f'\x1b[38;5;248m"\x1b[0m{dumps(data)[1:-1]}\x1b[38;5;248m"\x1b[0m') 152 153 154 def seems_url(s: str) -> bool: 155 protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') 156 return any(s.startswith(p) for p in protocols) 157 158 159 # type2func helps func restyle dispatch styler funcs, using values' types 160 # as the keys; while JSON doesn't have int values, the stdlib auto-promotes 161 # round JSON numbers into ints, and so this table has a func for those too 162 type2func = { 163 type(None): restyle_none, 164 bool: restyle_bool, 165 int: restyle_int, 166 float: restyle_float, 167 str: restyle_str, 168 list: restyle_list, 169 tuple: restyle_list, 170 dict: restyle_dict, 171 } 172 173 174 try: 175 if len(argv) < 2: 176 restyle(stdout, load(stdin)) 177 elif len(argv) == 2: 178 name = argv[1] 179 if name == '-': 180 restyle(stdout, load(stdin)) 181 elif seems_url(name): 182 from urllib.request import urlopen 183 with urlopen(name) as inp: 184 restyle(stdout, load(inp)) 185 else: 186 with open(name, encoding='utf-8') as inp: 187 restyle(stdout, load(inp)) 188 else: 189 raise ValueError('multiple inputs not allowed') 190 except BrokenPipeError: 191 # quit quietly, instead of showing a confusing error message 192 stderr.close() 193 except KeyboardInterrupt: 194 exit(2) 195 except Exception as e: 196 print(f'\x1b[31m{e}\x1b[0m', file=stderr) 197 exit(1)