Skip to content

Instantly share code, notes, and snippets.

@elunico
Last active June 9, 2025 11:19
Show Gist options
  • Save elunico/82acc447e4d3130a4159ad3153016309 to your computer and use it in GitHub Desktop.
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!
#!/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()
@elunico
Copy link
Author

elunico commented Jun 8, 2025

Example

$ curl -X 'GET' 'https://en.wikipedia.org/api/rest_v1/page/title/Microsoft_Windows' -H 'accept: application/json' | ./jscli.py -k items.0.title

Returns

Microsoft_Windows

Or

$ curl -X 'GET' 'https://en.wikipedia.org/api/rest_v1/page/title/Microsoft_Windows' -H 'accept: application/json' | ./jscli.py -k items.0 -l -s

Returns

Keys at level: items.0 (count: 13)
  |-title
  |-page_id
  |-rev
  |-tid
  |-namespace
  |-user_id
  |-user_text
  |-comment
  |-timestamp
  |-tags
  |-restrictions
  |-page_language
  |-redirect
==========

@elunico
Copy link
Author

elunico commented Jun 9, 2025

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