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