#!/usr/bin/env python3 """Jsonfmt — JSON formatter, validator, and diff tool.""" import argparse, json, sys, difflib, os def color(text, code): return f"\033[{code}m{text}\033[0m" if sys.stdout.isatty() else text def validate(args): errors = 0 for f in args.files: try: data = json.load(open(f)) print(f"{color('✓', '32')} {f} — valid JSON") if args.strict and isinstance(data, (list, dict)): json.dumps(data, ensure_ascii=args.strict_ascii) except (json.JSONDecodeError, FileNotFoundError) as e: print(f"{color('✗', '31')} {f} — {e}") errors += 1 return errors def format_file(args): for f in args.files: try: data = json.load(open(f)) indent = args.indent or 2 out = json.dumps(data, indent=indent, sort_keys=args.sort) if args.output: open(args.output, "w").write(out + "\n") print(f"{color('✓', '32')} Formatted → {args.output}") else: print(out) except (json.JSONDecodeError, FileNotFoundError) as e: print(f"{color('✗', '31')} {f} — {e}", file=sys.stderr) def minify(args): for f in args.files: try: data = json.load(open(f)) out = json.dumps(data, separators=(",", ":")) if args.output: open(args.output, "w").write(out + "\n") print(f"{color('✓', '32')} Minified → {args.output}") else: print(out) except Exception as e: print(f"{color('✗', '31')} {f} — {e}", file=sys.stderr) def diff_files(args): if len(args.files) < 2: print("diff requires two files", file=sys.stderr) return 1 a, b = args.files[:2] try: da = json.load(open(a)) db = json.load(open(b)) except Exception as e: print(f"{color('✗', '31')} {e}", file=sys.stderr) return 1 sa = json.dumps(da, indent=2, sort_keys=True) sb = json.dumps(db, indent=2, sort_keys=True) diff = difflib.unified_diff(sa.splitlines(), sb.splitlines(), fromfile=a, tofile=b, lineterm="") for line in diff: if line.startswith("+"): print(color(line, "32")) elif line.startswith("-"): print(color(line, "31")) elif line.startswith("@@"): print(color(line, "36")) else: print(line) return 0 def main(): parser = argparse.ArgumentParser( description="Jsonfmt — JSON formatter, validator, and diff tool", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" " jsonfmt validate data.json # Validate JSON\n" " jsonfmt format data.json # Pretty-print\n" " jsonfmt minify data.json # Minify\n" " jsonfmt diff a.json b.json # Diff two files\n" " cat data.json | jsonfmt format - # Read from stdin\n" ), ) parser.add_argument("--version", action="version", version="jsonfmt 1.0.0") sub = parser.add_subparsers(dest="command", required=True) v = sub.add_parser("validate", help="Validate JSON file(s)") v.add_argument("files", nargs="+", help="JSON files to validate") v.add_argument("--strict", action="store_true", help="Strict validation") v.add_argument("--strict-ascii", action="store_true", help="Enforce ASCII-only output") f = sub.add_parser("format", help="Pretty-print JSON") f.add_argument("files", nargs="+", help="JSON files to format") f.add_argument("--indent", type=int, help="Indent width") f.add_argument("--sort", action="store_true", help="Sort keys") f.add_argument("-o", "--output", help="Output file") m = sub.add_parser("minify", help="Minify JSON") m.add_argument("files", nargs="+", help="JSON files to minify") m.add_argument("-o", "--output", help="Output file") d = sub.add_parser("diff", help="Diff two JSON files") d.add_argument("files", nargs=2, help="Two JSON files to compare") args = parser.parse_args() if args.command == "validate": sys.exit(validate(args)) elif args.command == "format": format_file(args) elif args.command == "minify": minify(args) elif args.command == "diff": sys.exit(diff_files(args)) if __name__ == "__main__": main()