File: jk.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 load, dump 27 from sys import argv, exit, stderr, stdin, stdout 28 29 30 info = ''' 31 jk [filepath/URI...] 32 33 Json Keys loads JSON data and finds all its keys non-recursively. Its output 34 is always JSON Lines (JSONL). 35 36 When the top-level value is an object, the result is a JSON line with the all 37 top-level keys. Top-level arrays are checked non-recursively for objects, and 38 all unique arrays of keys are emitted as JSON Lines, without repeating. 39 ''' 40 41 # handle standard help cmd-line options, quitting right away in that case 42 if len(argv) > 1 and argv[1] in ('-h', '--h', '-help', '--help'): 43 print(info.strip()) 44 exit(0) 45 46 47 def output_line(w, data) -> None: 48 dump(data, w, indent=None, allow_nan=False, separators=(',', ': ')) 49 w.write('\n') 50 51 52 def json_keys(w, src, sort_keys) -> None: 53 data = load(src) 54 55 if isinstance(data, dict): 56 keys = sorted(data.keys()) if sort_keys else data.keys() 57 output_line(w, keys) 58 return 59 60 if not isinstance(data, (list, tuple)): 61 return 62 63 got = set() 64 65 for e in data: 66 if not isinstance(e, dict): 67 continue 68 69 keys = tuple(e.keys()) 70 if sort_keys: 71 keys = sorted(keys) 72 73 if keys in got: 74 continue 75 76 output_line(w, keys) 77 got.add(keys) 78 79 80 def seems_url(s: str) -> bool: 81 protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') 82 return any(s.startswith(p) for p in protocols) 83 84 85 args = argv[1:] 86 sort_keys = False 87 sort_opts = ('-s', '--s', '-sort', '--sort', '-sorted', '--sorted') 88 if len(args) > 0 and args[0] in sort_opts: 89 sort_keys = True 90 args = args[1:] 91 92 try: 93 if len(args) < 1: 94 json_keys(stdout, stdin.buffer, sort_keys) 95 elif len(args) == 1: 96 name = args[0] 97 if name == '-': 98 json_keys(stdout, stdin.buffer, sort_keys) 99 elif seems_url(name): 100 from urllib.request import urlopen 101 with urlopen(name) as inp: 102 json_keys(stdout, inp, sort_keys) 103 else: 104 with open(name, mode='rb') as inp: 105 json_keys(stdout, inp, sort_keys) 106 else: 107 raise ValueError('multiple inputs not allowed') 108 except BrokenPipeError: 109 # quit quietly, instead of showing a confusing error message 110 stderr.close() 111 except KeyboardInterrupt: 112 exit(2) 113 except Exception as e: 114 print(f'\x1b[31m{e}\x1b[0m', file=stderr) 115 exit(1)