Last active
June 9, 2025 11:19
-
-
Save elunico/82acc447e4d3130a4159ad3153016309 to your computer and use it in GitHub Desktop.
A bit of Python I slapped together to help me look through JSON on the command-line. Typical use case is to pipe curl into this program. Good for testing APIs!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import json | |
import sys | |
import argparse | |
import typing | |
def pprint_json(value: dict | list) -> str: | |
return json.dumps(value, indent=2, separators=(', ', ': '), sort_keys=options.sortkeys is not None) | |
def parse_path(kp: str) -> list[str]: | |
components = [] | |
i = 0 | |
stop = 0 | |
is_real_dot = False | |
while i < len(kp): | |
c = kp[i] | |
if is_real_dot and not (c == '.' or c == '\\'): | |
raise ValueError("Invalid escape sequence: \\{}".format(c)) | |
if (c == '.' or c == '\\') and is_real_dot: | |
i += 1 | |
is_real_dot = False | |
continue | |
if c == '\\': | |
is_real_dot = True | |
i += 1 | |
continue | |
if c == '.' and not is_real_dot: | |
components.append(kp[stop:i]) | |
stop = i + 1 # ignore . separator | |
i += 1 | |
continue | |
i += 1 | |
components.append(kp[stop:i]) | |
return components | |
def parse_args(): | |
ap = argparse.ArgumentParser(usage='./jscli.py [ -f FILE ] ( -k KEYS [ -s [ SEPARATOR ] ] [ -p ] ) | ( -l ) | ( -a )', | |
description='Parse and print data from JSON. Allows you to list keys, access values, access nested values, and elements in JSON arrays') | |
ap2 = ap.add_argument_group() | |
ap2.add_argument('-k', '--key', nargs='+', | |
help='Print the value associated with the key. Use . to access hierarchical keys. Use \\. for a real period in the key. Use integer values to access positions in a JSON array') | |
ap2.add_argument( | |
'-s', '--showkey', nargs='*', help='Print out the key that each value is associated with. Optionally provide a char after p as the key-value separator. Default is =. Due to limits with Python\'s argparse module, this appears to take many arguments but you should provide at most 1 string. ') | |
ap.add_argument('-l', '--list', action='store_true', | |
help='Print all the keys at the given level. If no -k is provided. Print all the keys at the top level. If a -k is given print all the keys at the level of object at k. No values will be printed') | |
ap.add_argument('-f', '--file', nargs='?', | |
help='JSON File to read and parse. If not provided, read from standard in') | |
ap.add_argument('-p', '--pretty', action='store_true', | |
help='Pretty print the output') | |
ap.add_argument('-a', '--all', action='store_true', | |
help='Pretty print the entire JSON file to stdout. Ignores all other formatting and output options') | |
ap.add_argument('-t', '--sortkeys', action='store_true', | |
help='Sort keys before printing. Only applies when pretty-printing using -p') | |
args = ap.parse_args() | |
if not args.key and not args.list and not args.all: | |
ap.error( | |
"Specify a key path using -k or the -l flag to list all top level keys or -a to pretty print the entire JSON object") | |
return args | |
def format_option(value: typing.Any, key_path: str) -> str: | |
def elaborated(): | |
return (options.showkey is not None or options.pretty) | |
result = [] | |
if elaborated(): | |
if isinstance(value, dict): | |
ks = value.keys() | |
lstr = f' (count: {len(ks)})' | |
else: | |
lstr = '' | |
result.append("Keys at level: {}{}".format(key_path, lstr)) | |
if isinstance(value, list): | |
result.append( | |
f'*** [{key_path}] is an array of length {len(value)} ***') | |
elif isinstance(value, dict): | |
for k in value.keys(): | |
if elaborated(): | |
result.append(f' |-{k}') | |
else: | |
result.append(k) | |
else: | |
result.append(f'*** [{key_path}] IS AN ATOMIC ***') | |
if elaborated(): | |
result.append('='*10) | |
return '\n'.join(result) | |
def format_value(value: typing.Any, key_path: str) -> str: | |
result = [] | |
if options.showkey is not None: | |
if not options.showkey: | |
result.append(key_path) | |
result.append('=') | |
else: | |
if len(options.showkey) > 1: | |
raise ValueError( | |
'Duplicate -p argument. Specify only one string to act as the key-value separator') | |
result.append(key_path) | |
result.append(options.showkey[0]) | |
if options.pretty: | |
result.append(pprint_json(value)) | |
result.append('\n') | |
else: | |
result.append(str(value)) | |
result.append('\n') | |
return ''.join(result) | |
def get_text(options) -> str: | |
if options.file is None: | |
text = sys.stdin.read() | |
else: | |
with open(options.file) as f: | |
text = f.read() | |
return text | |
def main(): | |
global options | |
options = parse_args() | |
text = get_text(options) | |
obj = json.loads(text) | |
if options.all: | |
print(pprint_json(obj)) | |
return | |
if not options.key: | |
print(format_option(obj, '<root>')) | |
return | |
for key_path in options.key: | |
path = parse_path(key_path) | |
value = obj | |
for (idx, k) in enumerate(path): | |
try: | |
k = int(k) | |
except ValueError: | |
pass | |
try: | |
value = value[k] | |
except IndexError: | |
message = f'Index {k} is out of range for value with key \'{".".join(path[:idx])}\'' | |
raise IndexError(message) from None | |
if not options.list: # print values and separators | |
print(format_value(value, key_path)) | |
else: # print keys with associated headers | |
print(format_option(value, key_path)) | |
if __name__ == '__main__': | |
main() |
For reference: the help print out
usage: ./jscli.py [ -f FILE ] ( -k KEYS [ -s [ SEPARATOR ] ] [ -p ] [ -t ] ) | ( -l ) | ( -a )
Parse and print data from JSON. Allows you to list keys, access values, access nested values, and elements in JSON arrays
options:
-h, --help show this help message and exit
-l, --list Print all the keys at the given level. If no -k is provided. Print all the keys at the top level.
If a -k is given print all the keys at the level of object at k. No values will be printed
-f, --file [FILE] JSON File to read and parse. If not provided, read from standard in
-p, --pretty Pretty print the output
-a, --all Pretty print the entire JSON file to stdout. Ignores all other formatting and output options
-t, --sortkeys Sort keys before printing. Only applies when pretty-printing using -p
-k, --key KEY [KEY ...]
Print the value associated with the key. Use . to access hierarchical keys. Use \. for a real
period in the key. Use integer values to access positions in a JSON array
-s, --showkey [SHOWKEY ...]
Print out the key that each value is associated with. Optionally provide a char after p as the
key-value separator. Default is =. Due to limits with Python's argparse module, this appears to
take many arguments but you should provide at most 1 string.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example
Returns
Or
Returns