264 lines
7.8 KiB
Python
Executable File
264 lines
7.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
|
||
# ============================================================================
|
||
# JSON Validator Script
|
||
# ============================================================================
|
||
# Purpose: Multi-backend JSON syntax validation with detailed error reporting
|
||
# Version: 1.0.0
|
||
# Usage: ./json-validator.py --file <path> [--verbose]
|
||
# Returns: 0=valid, 1=invalid, 2=error
|
||
# Backends: jq (preferred), python3 json module (fallback)
|
||
# ============================================================================
|
||
|
||
import json
|
||
import sys
|
||
import argparse
|
||
import subprocess
|
||
import shutil
|
||
from pathlib import Path
|
||
|
||
|
||
# ====================
|
||
# Color Support
|
||
# ====================
|
||
|
||
class Colors:
|
||
"""ANSI color codes for terminal output"""
|
||
RED = '\033[0;31m'
|
||
GREEN = '\033[0;32m'
|
||
YELLOW = '\033[1;33m'
|
||
BLUE = '\033[0;34m'
|
||
CYAN = '\033[0;36m'
|
||
BOLD = '\033[1m'
|
||
NC = '\033[0m'
|
||
|
||
@classmethod
|
||
def disable(cls):
|
||
"""Disable colors for non-TTY output"""
|
||
cls.RED = cls.GREEN = cls.YELLOW = cls.BLUE = cls.CYAN = cls.BOLD = cls.NC = ''
|
||
|
||
|
||
if not sys.stdout.isatty():
|
||
Colors.disable()
|
||
|
||
|
||
# ====================
|
||
# Backend Detection
|
||
# ====================
|
||
|
||
def detect_backend():
|
||
"""Detect available JSON validation backend"""
|
||
if shutil.which('jq'):
|
||
return 'jq'
|
||
elif sys.version_info >= (3, 0):
|
||
return 'python3'
|
||
else:
|
||
return 'none'
|
||
|
||
|
||
def print_backend_info():
|
||
"""Print detected backend information"""
|
||
backend = detect_backend()
|
||
if backend == 'jq':
|
||
print(f"{Colors.GREEN}✅ Backend: jq (preferred){Colors.NC}")
|
||
elif backend == 'python3':
|
||
print(f"{Colors.YELLOW}⚠️ Backend: python3 (fallback){Colors.NC}")
|
||
else:
|
||
print(f"{Colors.RED}❌ No JSON validator available{Colors.NC}")
|
||
print(f"{Colors.BLUE}ℹ️ Install jq for better error messages: apt-get install jq{Colors.NC}")
|
||
return backend
|
||
|
||
|
||
# ====================
|
||
# JQ Backend
|
||
# ====================
|
||
|
||
def validate_with_jq(file_path, verbose=False):
|
||
"""Validate JSON using jq (provides better error messages)"""
|
||
try:
|
||
result = subprocess.run(
|
||
['jq', 'empty', file_path],
|
||
capture_output=True,
|
||
text=True,
|
||
check=False
|
||
)
|
||
|
||
if result.returncode == 0:
|
||
print(f"{Colors.GREEN}✅ Valid JSON: {file_path}{Colors.NC}")
|
||
print(f"Backend: jq")
|
||
return 0
|
||
else:
|
||
# Parse jq error message
|
||
error_msg = result.stderr.strip()
|
||
print(f"{Colors.RED}❌ Invalid JSON: {file_path}{Colors.NC}")
|
||
|
||
if verbose:
|
||
print(f"{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
|
||
print(f"{Colors.RED}Error Details:{Colors.NC}")
|
||
print(f" {error_msg}")
|
||
print()
|
||
print(f"{Colors.YELLOW}Remediation:{Colors.NC}")
|
||
print(" - Check for missing commas between object properties")
|
||
print(" - Verify bracket matching: [ ] { }")
|
||
print(" - Ensure proper string quoting")
|
||
print(" - Use a JSON formatter/linter in your editor")
|
||
else:
|
||
# Extract line number if available
|
||
if "parse error" in error_msg.lower():
|
||
print(f"Error: {error_msg}")
|
||
|
||
return 1
|
||
|
||
except FileNotFoundError:
|
||
print(f"{Colors.RED}❌ File not found: {file_path}{Colors.NC}", file=sys.stderr)
|
||
return 2
|
||
except Exception as e:
|
||
print(f"{Colors.RED}❌ Error running jq: {e}{Colors.NC}", file=sys.stderr)
|
||
return 2
|
||
|
||
|
||
# ====================
|
||
# Python3 Backend
|
||
# ====================
|
||
|
||
def validate_with_python(file_path, verbose=False):
|
||
"""Validate JSON using Python's json module (universal fallback)"""
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
# Attempt to parse JSON
|
||
json.loads(content)
|
||
|
||
print(f"{Colors.GREEN}✅ Valid JSON: {file_path}{Colors.NC}")
|
||
print(f"Backend: python3")
|
||
return 0
|
||
|
||
except FileNotFoundError:
|
||
print(f"{Colors.RED}❌ File not found: {file_path}{Colors.NC}", file=sys.stderr)
|
||
return 2
|
||
|
||
except json.JSONDecodeError as e:
|
||
print(f"{Colors.RED}❌ Invalid JSON: {file_path}{Colors.NC}")
|
||
|
||
if verbose:
|
||
print(f"{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
|
||
print(f"{Colors.RED}Error Details:{Colors.NC}")
|
||
print(f" Line: {e.lineno}")
|
||
print(f" Column: {e.colno}")
|
||
print(f" Issue: {e.msg}")
|
||
print()
|
||
|
||
# Show problematic section
|
||
try:
|
||
lines = content.split('\n')
|
||
start = max(0, e.lineno - 3)
|
||
end = min(len(lines), e.lineno + 2)
|
||
|
||
print(f"Problematic Section (lines {start+1}-{end}):")
|
||
for i in range(start, end):
|
||
line_num = i + 1
|
||
marker = "→" if line_num == e.lineno else " "
|
||
print(f" {marker} {line_num:3d} | {lines[i]}")
|
||
|
||
print()
|
||
except:
|
||
pass
|
||
|
||
print(f"{Colors.YELLOW}Remediation:{Colors.NC}")
|
||
print(" - Check for missing commas between array elements or object properties")
|
||
print(" - Verify bracket matching: [ ] { }")
|
||
print(" - Ensure all strings are properly quoted")
|
||
print(" - Use a JSON formatter/linter in your editor")
|
||
else:
|
||
print(f"Error: {e.msg} at line {e.lineno}, column {e.colno}")
|
||
|
||
return 1
|
||
|
||
except Exception as e:
|
||
print(f"{Colors.RED}❌ Error reading file: {e}{Colors.NC}", file=sys.stderr)
|
||
return 2
|
||
|
||
|
||
# ====================
|
||
# Main Validation
|
||
# ====================
|
||
|
||
def validate_json(file_path, verbose=False):
|
||
"""Main validation function with backend selection"""
|
||
backend = detect_backend()
|
||
|
||
if backend == 'none':
|
||
print(f"{Colors.RED}❌ No JSON validation backend available{Colors.NC}", file=sys.stderr)
|
||
print(f"{Colors.BLUE}ℹ️ Install jq or ensure python3 is available{Colors.NC}", file=sys.stderr)
|
||
return 2
|
||
|
||
if backend == 'jq':
|
||
return validate_with_jq(file_path, verbose)
|
||
else:
|
||
return validate_with_python(file_path, verbose)
|
||
|
||
|
||
# ====================
|
||
# CLI Interface
|
||
# ====================
|
||
|
||
def main():
|
||
"""CLI entry point"""
|
||
parser = argparse.ArgumentParser(
|
||
description='Validate JSON file syntax with multi-backend support',
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog='''
|
||
Examples:
|
||
./json-validator.py --file plugin.json
|
||
./json-validator.py --file marketplace.json --verbose
|
||
./json-validator.py --detect
|
||
|
||
Backends:
|
||
- jq (preferred): Fast, excellent error messages
|
||
- python3 (fallback): Universal availability
|
||
|
||
Exit codes:
|
||
0: Valid JSON
|
||
1: Invalid JSON
|
||
2: File error or backend unavailable
|
||
'''
|
||
)
|
||
|
||
parser.add_argument(
|
||
'--file',
|
||
type=str,
|
||
help='Path to JSON file to validate'
|
||
)
|
||
|
||
parser.add_argument(
|
||
'--verbose',
|
||
action='store_true',
|
||
help='Show detailed error information and remediation'
|
||
)
|
||
|
||
parser.add_argument(
|
||
'--detect',
|
||
action='store_true',
|
||
help='Detect and display available backend'
|
||
)
|
||
|
||
args = parser.parse_args()
|
||
|
||
# Handle backend detection
|
||
if args.detect:
|
||
print_backend_info()
|
||
return 0
|
||
|
||
# Validate required arguments
|
||
if not args.file:
|
||
parser.print_help()
|
||
return 2
|
||
|
||
# Perform validation
|
||
return validate_json(args.file, args.verbose)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|