Skip to content

Instantly share code, notes, and snippets.

@jroelofs
Last active July 31, 2020 18:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jroelofs/d36a2e81edcc0e08baade26e8756011b to your computer and use it in GitHub Desktop.
Save jroelofs/d36a2e81edcc0e08baade26e8756011b to your computer and use it in GitHub Desktop.
JSON diff tool
#!/usr/bin/env python3
# Finds the structural diff of two json files, ignoring formatting
# changes that a normal diff would highlight.
import json
import sys
def usage():
print("usage:\n\t{} <lhs.json> <rhs.json>".format(sys.argv[0]))
try:
if not sys.stdout.isatty():
raise ValueError("not a tty")
from termcolor import cprint, colored
red = lambda s: colored(s, 'red')
grn = lambda s: colored(s, 'green')
yellow = lambda s: colored(s, 'yellow')
except:
cprint = print
red = lambda s: s
grn = lambda s: s
yellow = lambda s: s
class DiffReason(object):
def dump(self):
pass
def dump_path(self, key_path):
cprint(yellow("@@ " + "[" + ("][".join(key_path)) + "]" + " @@"))
class ValueChange(DiffReason):
def __init__(self, key_path, lhs, rhs):
self.key_path = key_path
self.lhs = lhs
self.rhs = rhs
def dump(self):
self.dump_path(self.key_path)
cprint(red("- " + str(self.lhs)))
cprint(grn("+ " + str(self.rhs)))
if isinstance(self.lhs, int) or \
isinstance(self.lhs, float):
cprint(yellow("Δ") + " {}".format(self.rhs - self.lhs))
class KeyChange(DiffReason):
def __init__(self, key_path, value, is_insert):
self.key_path = key_path
self.value = value
self.is_insert = is_insert
def dump(self):
self.dump_path(self.key_path)
if self.is_insert:
cprint(grn("+ " + str(self.value)))
else:
cprint(red("- " + str(self.value)))
def diff(key_path, lhs, rhs):
if type(lhs) != type(rhs):
if lhs != rhs:
yield ValueChange(key_path, lhs, rhs)
return
if type(rhs) == dict:
for k in sorted(rhs.keys()):
if k not in lhs.keys():
yield KeyChange(key_path + ["'{}'".format(k)], rhs[k], True)
for k in sorted(lhs.keys()):
if k not in rhs.keys():
yield KeyChange(key_path + ["'{}'".format(k)], lhs[k], False)
for k in sorted(lhs.keys()):
if k in rhs.keys():
yield from diff(key_path + ["'{}'".format(k)], lhs[k], rhs[k])
return
if type(rhs) == list:
if len(lhs) != len(rhs):
yield ValueChange(key_path, lhs, rhs)
return
for k in range(len(lhs)):
yield from diff(key_path + [str(k)], lhs[k], rhs[k])
return
if lhs == rhs:
return
yield ValueChange(key_path, lhs, rhs)
return
def main():
if len(sys.argv) != 3:
usage()
sys.exit(-1)
lhs = {}
with open(sys.argv[1], "r") as lhs_file:
lhs = json.loads(lhs_file.read())
rhs = {}
with open(sys.argv[2], "r") as rhs_file:
rhs = json.loads(rhs_file.read())
change_count = 0
for change in diff([], lhs, rhs):
if change_count == 0:
cprint(red("--- " + sys.argv[1]))
cprint(grn("+++ " + sys.argv[2]))
change_count += 1
change.dump()
sys.exit(min(change_count, 255))
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment