72 lines
2.0 KiB
Python
72 lines
2.0 KiB
Python
#!/usr/bin/env python3
|
|
"""json_diff.py - Compare two JSON files for structural equality.
|
|
|
|
Usage: python3 json_diff.py <file1.json> <file2.json>
|
|
|
|
Returns exit code 0 if JSON structures match (ignoring key order in objects),
|
|
1 if they differ, 2 on error. Outputs the diff to stdout.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def normalize_json(data):
|
|
"""Recursively normalize JSON: sort dict keys, convert lists."""
|
|
if isinstance(data, dict):
|
|
return {k: normalize_json(data[k]) for k in sorted(data.keys())}
|
|
elif isinstance(data, list):
|
|
return [normalize_json(item) for item in data]
|
|
else:
|
|
return data
|
|
|
|
|
|
def json_equal(file1: str, file2: str) -> tuple:
|
|
"""Compare two JSON files. Returns (equal: bool, diff: str, error: str|None)."""
|
|
try:
|
|
data1 = json.loads(Path(file1).read_text())
|
|
data2 = json.loads(Path(file2).read_text())
|
|
|
|
norm1 = normalize_json(data1)
|
|
norm2 = normalize_json(data2)
|
|
|
|
if norm1 == norm2:
|
|
return True, "", None
|
|
|
|
diff_lines = []
|
|
diff_lines.append(f"Files differ:")
|
|
diff_lines.append(f" {file1}: {json.dumps(norm1, indent=2)[:500]}")
|
|
diff_lines.append(f" {file2}: {json.dumps(norm2, indent=2)[:500]}")
|
|
|
|
return False, "\n".join(diff_lines), None
|
|
|
|
except FileNotFoundError as e:
|
|
return False, "", f"File not found: {e.filename}"
|
|
except json.JSONDecodeError as e:
|
|
return False, "", f"JSON error in {e.doc}: line {e.lineno}"
|
|
except Exception as e:
|
|
return False, "", str(e)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
print("Usage: python3 json_diff.py <file1.json> <file2.json>", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
result, diff, error = json_equal(sys.argv[1], sys.argv[2])
|
|
|
|
if error:
|
|
print(f"Error: {error}", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
if result:
|
|
print("STRUCTURALLY EQUAL")
|
|
sys.exit(0)
|
|
else:
|
|
print(diff)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |