#!/usr/bin/python3 # The MIT License (MIT) # # Copyright © 2020-2025 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 re import compile as compile_re from sys import argv, exit, stderr, stdin, stdout from typing import List, Union info = ''' ngron [options...] [filepath/URI...] Nice GRON converts JSON data into `grep`-friendly lines, similar to what tool `gron` (GRep jsON; https://github.com/tomnomnom/gron) does. This tool uses `nicer` ANSI styles than the original, hence its name, but can't convert its output back into JSON, unlike the latter. Unlike the original `gron`, there's no sort-mode. When not given a named source (filepath/URI) to read from, data are read from standard input. Options, where leading double-dashes are also allowed: -h show this help message -help show this help message -m monochrome (default), enables unstyled output-mode -c color, enables ANSI-styled output-mode -color enables ANSI-styled output-mode ''' # handle standard help cmd-line options, quitting right away in that case if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): print(info.strip()) exit(0) # simple_str_re helps funcs restyle_path and emit_path figure out which # strings need quoting simple_str_re = compile_re('[a-zA-Z_][a-zA-Z_0-9]*') def gron_path(w, path: List[Union[int, str]]) -> None: w.write('json') for e in path: if isinstance(e, int): w.write(f'[{e}]') continue if simple_str_re.match(e): w.write(f'.{e}') continue w.write(f'[{dumps(e)}]') w.write(' = ') def restyle_path(w, path: List[Union[int, str]]) -> None: w.write('\x1b[38;2;135;95;255mjson') syntax = False for e in path: if isinstance(e, int): w.write('\x1b[38;2;168;168;168m[\x1b[38;2;0;135;95m') w.write(str(e)) w.write('\x1b[38;2;168;168;168m]') syntax = True continue if simple_str_re.match(e): w.write(f'\x1b[38;2;168;168;168m.\x1b[38;2;135;95;255m{e}') syntax = False continue w.write('\x1b[38;2;168;168;168m["\x1b[38;2;0;95;135m') w.write(dumps(e)[1:-1]) w.write('\x1b[38;2;168;168;168m"]') syntax = True if syntax: w.write(' = ') else: w.write('\x1b[38;2;168;168;168m = ') def end_styled_line(w) -> None: w.write('\x1b[38;2;168;168;168m;\x1b[0m\n') def colored_gron(w, data, path: List[Union[int, str]]) -> None: 'This func is where all the recursive output-action starts/happens.' f = type2ansi[type(data)] if f != None: f(w, data, path) else: raise ValueError(f'unsupported type {type(data)}') def restyle_bool(w, data: bool, path: List[Union[int, str]]) -> None: 'Handle booleans for func restyle.' restyle_path(w, path) if data: w.write('\x1b[38;2;95;175;215mtrue') else: w.write('\x1b[38;2;95;175;215mfalse') end_styled_line(w) def restyle_dict(w, data: dict, path: List[Union[int, str]]) -> None: 'Handle dictionaries for func restyle.' restyle_path(w, path) w.write('\x1b[38;2;168;168;168m{};\x1b[0m\n') path.append('') for k, v in data.items(): path[-1] = k colored_gron(w, v, path) del path[-1] def restyle_float(w, data: float, path: List[Union[int, str]]) -> None: 'Handle floats for func restyle, emitting as many decimals as needed.' restyle_path(w, path) # n = int(data) # if float(n) != data: # w.write(f'\x1b[38;2;0;135;95m{data:-1}\x1b[0m') # else: # w.write(f'\x1b[38;2;0;135;95m{n}\x1b[0m') w.write(f'\x1b[38;2;0;135;95m{data:-1}\x1b[0m') end_styled_line(w) def restyle_int(w, data: int, path: List[Union[int, str]]) -> None: 'Handle integers for func restyle.' restyle_path(w, path) w.write(f'\x1b[38;2;0;135;95m{data}\x1b[0m') end_styled_line(w) def restyle_list(w, data: list, path: List[Union[int, str]]) -> None: 'Handle lists for func restyle.' restyle_path(w, path) w.write('\x1b[38;2;168;168;168m[];\x1b[0m\n') path.append(0) for i, v in enumerate(data): path[-1] = i colored_gron(w, v, path) del path[-1] def restyle_none(w, data: bool, path: List[Union[int, str]]) -> None: 'Handle nulls for func restyle.' restyle_path(w, path) w.write('\x1b[38;2;168;168;168mnull') end_styled_line(w) def restyle_str(w, data: str, path: List[Union[int, str]]) -> None: 'Handle strings for func restyle.' restyle_path(w, path) w.write(f'\x1b[38;2;168;168;168m"\x1b[0m') w.write(dumps(data)[1:-1]) w.write(f'\x1b[38;2;168;168;168m"\x1b[0m') end_styled_line(w) def gron(w, data, path: List[Union[int, str]]) -> None: 'Recursively output data into gron-format lines.' f = type2gron[type(data)] if f != None: f(w, data, path) else: raise ValueError(f'unsupported type {type(data)}') def gron_bool(w, data: bool, path: List[Union[int, str]]) -> None: 'Handle booleans for func gron.' gron_path(w, path) if data: w.write('true;\n') else: w.write('false;\n') def gron_dict(w, data: dict, path: List[Union[int, str]]) -> None: 'Handle dictionaries for func gron.' gron_path(w, path) w.write('{};\n') path.append('') for k, v in data.items(): path[-1] = k gron(w, v, path) del path[-1] def gron_float(w, data: float, path: List[Union[int, str]]) -> None: 'Handle floats for func gron, emitting as many decimals as needed.' gron_path(w, path) # n = int(data) # if float(n) != data: # w.write(f'{data:-1};\n') # else: # w.write(f'{n};\n') w.write(f'{data:-1};\n') def gron_int(w, data: int, path: List[Union[int, str]]) -> None: 'Handle integers for func gron.' gron_path(w, path) w.write(f'{data};\n') def gron_list(w, data: list, path: List[Union[int, str]]) -> None: 'Handle lists for func gron.' gron_path(w, path) w.write('[];\n') path.append(0) for i, v in enumerate(data): path[-1] = i gron(w, v, path) del path[-1] def gron_none(w, data: bool, path: List[Union[int, str]]) -> None: 'Handle nulls for func gron.' gron_path(w, path) w.write('null;\n') def gron_str(w, data: str, path: List[Union[int, str]]) -> None: 'Handle strings for func gron.' gron_path(w, path) w.write(f'{dumps(data)};\n') def seems_url(s: str) -> bool: protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') return any(s.startswith(p) for p in protocols) # type2ansi 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 type2ansi = { 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, } # type2gron helps func emit_gron dispatch emitter 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 type2gron = { type(None): gron_none, bool: gron_bool, int: gron_int, float: gron_float, str: gron_str, list: gron_list, tuple: gron_list, dict: gron_dict, } if len(argv) > 1 and argv[1].startswith('-'): l = argv[1].lstrip('-').lower() if l in ('c', 'color', 'nice'): emit = colored_gron args = argv[2:] elif l in ('g', 'gron', 'm', 'monochrome', 'p', 'plain'): emit = gron args = argv[2:] else: emit = gron args = argv[1:] else: args = argv[1:] emit = gron try: if len(args) > 1: raise ValueError('multiple inputs not allowed') name = args[0] if len(args) > 0 else '-' if name == '-': emit(stdout, load(stdin), []) elif seems_url(name): from urllib.request import urlopen with urlopen(name) as inp: emit(stdout, load(inp), []) else: with open(name, encoding='utf-8') as inp: emit(stdout, load(inp), []) 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)