#!/usr/bin/python3 # The MIT License (MIT) # # Copyright © 2024 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. from json import dumps, load from math import modf from sys import argv, exit, stderr, stdin, stdout info = ''' nj [filepath/URI...] Nice Json indents and styles JSON data. When not given a filepath to read data from, this tool reads from standard input. ''' if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): print(info.strip(), file=stderr) exit(0) # level2indent speeds up func indent level2indent = tuple(i * ' ' for i in range(64)) def indent(w, level: int) -> None: 'Emit 2 spaces for each indentation level.' if level < 1: return if level < len(level2indent): w.write(level2indent[level]) return while level >= len(level2indent): w.write(level2indent[-1]) level -= len(level2indent) w.write(level2indent[level]) def restyle(w, data, pre: int = 0, level: int = 0) -> None: 'This func is where all the recursive output-action starts/happens.' f = type2func[type(data)] if f != None: f(w, data, pre, level) # don't forget to end the final line, if at `top-level` if level == 0: w.write('\n') else: raise ValueError(f'unsupported type {type(data)}') def restyle_bool(w, data: bool, pre: int, level: int) -> None: indent(w, pre) if data: w.write('\x1b[38;5;74mtrue\x1b[0m') else: w.write('\x1b[38;5;74mfalse\x1b[0m') def restyle_dict(w, data: dict, pre: int, level: int) -> None: if len(data) == 0: indent(w, pre) w.write('\x1b[38;5;248m{}\x1b[0m') return indent(w, pre) w.write('\x1b[38;5;248m{\x1b[0m\n') for (i, k) in enumerate(data): if i > 0: w.write('\x1b[38;5;248m,\x1b[0m\n') restyle_key(w, k, level + 1) restyle(w, data[k], 0, level + 1) w.write('\n') indent(w, level) w.write('\x1b[38;5;248m}\x1b[0m') def restyle_float(w, data: float, pre: int, level: int) -> None: indent(w, pre) # can't figure out a direct float-format option which both avoids the # chance of scientific notation (invalid as JSON), and the chance of # unneeded trailing decimal 0s; neither {data:-1} nor {data:-1f} work if modf(data)[0] == 0.0: w.write(f'\x1b[38;5;29m{data}\x1b[0m') else: s = f'{data:-1f}'.rstrip('0') w.write(f'\x1b[38;5;29m{s}\x1b[0m') def restyle_int(w, data: int, pre: int, level: int) -> None: indent(w, pre) w.write(f'\x1b[38;5;29m{data}\x1b[0m') def restyle_key(w, k: str, level: int) -> None: indent(w, level) f = dumps w.write(f'\x1b[38;5;248m"\x1b[38;5;99m{f(k)[1:-1]}\x1b[38;5;248m": \x1b[0m') def restyle_list(w, data: list, pre: int, level: int) -> None: if len(data) == 0: indent(w, pre) w.write('\x1b[38;5;248m[]\x1b[0m') return indent(w, pre) w.write('\x1b[38;5;248m[\x1b[0m\n') for (i, e) in enumerate(data): if i > 0: w.write('\x1b[38;5;248m,\x1b[0m\n') restyle(w, e, level + 1, level + 1) w.write('\n') indent(w, level) w.write('\x1b[38;5;248m]\x1b[0m') def restyle_none(w, data: bool, pre: int, level: int) -> None: indent(w, pre) w.write('\x1b[38;5;248mnull\x1b[0m') def restyle_str(w, data: str, pre: int, level: int) -> None: indent(w, pre) # f'\x1b[38;5;248m"\x1b[38;5;24m{dumps(data)[1:-1]}\x1b[38;5;248m"\x1b[0m' w.write(f'\x1b[38;5;248m"\x1b[0m{dumps(data)[1:-1]}\x1b[38;5;248m"\x1b[0m') def seems_url(s: str) -> bool: protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') return any(s.startswith(p) for p in protocols) # type2func helps func restyle dispatch styler funcs, using values' types # as the keys; while JSON doesn't have int values, the stdlib auto-promotes # round JSON numbers into ints, and so this table has a func for those too type2func = { type(None): restyle_none, bool: restyle_bool, int: restyle_int, float: restyle_float, str: restyle_str, list: restyle_list, tuple: restyle_list, dict: restyle_dict, } try: if len(argv) < 2: restyle(stdout, load(stdin)) elif len(argv) == 2: name = argv[1] if name == '-': restyle(stdout, load(stdin)) elif seems_url(name): from urllib.request import urlopen with urlopen(name) as inp: restyle(stdout, load(inp)) else: with open(name, encoding='utf-8') as inp: restyle(stdout, load(inp)) else: raise ValueError('multiple inputs not allowed') except BrokenPipeError: # quit quietly, instead of showing a confusing error message stderr.close() except KeyboardInterrupt: exit(2) except Exception as e: print(f'\x1b[31m{e}\x1b[0m', file=stderr) exit(1)