#!/usr/bin/env python3 """json_diff.py - Compare two JSON files for structural equality. Usage: python3 json_diff.py 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 ", 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()