Last active
July 13, 2025 19:23
-
-
Save ashwch/cd87eb1574b1b88d21ddef1508a186f6 to your computer and use it in GitHub Desktop.
Bruno environment file validator - Validate .bru files for correct format
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # /// script | |
| # dependencies = [] | |
| # /// | |
| """ | |
| Bruno environment file validator. | |
| Validates .bru files for correct Bruno API client format. | |
| Usage: | |
| python validate_bruno_files.py <file_or_directory> | |
| Examples: | |
| python validate_bruno_files.py ./environments/ | |
| python validate_bruno_files.py ./env.bru | |
| """ | |
| import sys | |
| from pathlib import Path | |
| import re | |
| def validate_bruno_file(file_path): | |
| """ | |
| Validate a Bruno .bru environment file format. | |
| Returns: | |
| tuple: (is_valid: bool, issues: list[str]) | |
| """ | |
| issues = [] | |
| try: | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| except Exception as e: | |
| return False, [f"Cannot read file: {e}"] | |
| # Check for empty file | |
| if not content.strip(): | |
| return False, ["File is empty"] | |
| # Check for meta block with name field | |
| meta_pattern = r'meta\s*\{[^}]*name:\s*[\w\-\.]+[^}]*\}' | |
| if not re.search(meta_pattern, content, re.DOTALL): | |
| issues.append("Missing or invalid 'meta' block with 'name' field") | |
| # Check for vars block | |
| vars_pattern = r'vars\s*\{[^}]*\}' | |
| if not re.search(vars_pattern, content, re.DOTALL): | |
| issues.append("Missing 'vars' block") | |
| # Validate vars block content | |
| # Find the vars block more carefully to handle escaped braces | |
| vars_start = content.find('vars') | |
| if vars_start != -1: | |
| # Find the opening brace | |
| open_brace = content.find('{', vars_start) | |
| if open_brace != -1: | |
| # Find matching closing brace, accounting for escaped braces | |
| brace_count = 1 | |
| i = open_brace + 1 | |
| while i < len(content) and brace_count > 0: | |
| if content[i] == '\\' and i + 1 < len(content): | |
| i += 2 # Skip escaped character | |
| continue | |
| elif content[i] == '{': | |
| brace_count += 1 | |
| elif content[i] == '}': | |
| brace_count -= 1 | |
| i += 1 | |
| if brace_count == 0: | |
| vars_content = content[open_brace + 1:i - 1] | |
| vars_match = True | |
| else: | |
| vars_match = None | |
| vars_content = None | |
| else: | |
| vars_match = None | |
| else: | |
| vars_match = None | |
| if vars_match and vars_content: | |
| lines = [line.strip() for line in vars_content.strip().split('\n') if line.strip()] | |
| for line_num, line in enumerate(lines, 1): | |
| # Skip empty lines | |
| if not line: | |
| continue | |
| # Check for key: value format | |
| if ':' not in line: | |
| issues.append(f"Invalid variable format at line ~{line_num}: '{line[:30]}...' (missing colon)") | |
| continue | |
| # Split only on first colon to handle values with colons | |
| key, value = line.split(':', 1) | |
| key = key.strip() | |
| value = value.strip() | |
| # Validate key format (alphanumeric, underscore, hyphen) | |
| if not re.match(r'^[\w\-]+$', key): | |
| issues.append(f"Invalid variable name '{key}' (use only letters, numbers, underscore, hyphen)") | |
| # Check for proper quote handling in values | |
| if value: | |
| # For quoted strings, check if properly wrapped | |
| if value.startswith('"'): | |
| if not value.endswith('"'): | |
| issues.append(f"Unclosed quoted value in variable '{key}'") | |
| elif len(value) > 1 and value.endswith('\\"'): | |
| # Check if the ending quote is escaped | |
| issues.append(f"Value ends with escaped quote in variable '{key}'") | |
| elif value.endswith('"') and not value.startswith('"'): | |
| issues.append(f"Value has closing quote but no opening quote in variable '{key}'") | |
| # Check overall brace balance (excluding escaped braces) | |
| temp_content = content.replace('\\{', '').replace('\\}', '') | |
| open_braces = temp_content.count('{') | |
| close_braces = temp_content.count('}') | |
| if open_braces != close_braces: | |
| issues.append(f"Unbalanced braces in file: {open_braces} open vs {close_braces} close") | |
| # Check for required structure | |
| if open_braces < 2 or close_braces < 2: | |
| issues.append("File must contain at least meta{} and vars{} blocks") | |
| return len(issues) == 0, issues | |
| def main(): | |
| """Validate Bruno .bru files.""" | |
| if len(sys.argv) < 2: | |
| print("Usage: python validate_bruno_files.py <file_or_directory>") | |
| print("\nExamples:") | |
| print(" Single file: python validate_bruno_files.py env.bru") | |
| print(" Directory: python validate_bruno_files.py ./environments/") | |
| sys.exit(1) | |
| path = Path(sys.argv[1]) | |
| # Determine if validating single file or directory | |
| if path.is_file(): | |
| if not path.suffix == '.bru': | |
| print(f"β Error: {path} is not a .bru file") | |
| sys.exit(1) | |
| bru_files = [path] | |
| elif path.is_dir(): | |
| bru_files = list(path.glob("*.bru")) | |
| if not bru_files: | |
| print(f"β No .bru files found in {path}") | |
| sys.exit(1) | |
| else: | |
| print(f"β Error: {path} does not exist") | |
| sys.exit(1) | |
| # Validate files | |
| print(f"\nπ Validating {len(bru_files)} Bruno file(s)...\n") | |
| valid_count = 0 | |
| invalid_files = [] | |
| for bru_file in sorted(bru_files): | |
| is_valid, issues = validate_bruno_file(bru_file) | |
| if is_valid: | |
| print(f"β {bru_file.name}") | |
| valid_count += 1 | |
| else: | |
| print(f"β {bru_file.name}") | |
| invalid_files.append((bru_file.name, issues)) | |
| for issue in issues[:3]: # Show first 3 issues | |
| print(f" ββ {issue}") | |
| if len(issues) > 3: | |
| print(f" ββ ... and {len(issues) - 3} more issue(s)") | |
| # Summary | |
| print(f"\nπ Summary: {valid_count}/{len(bru_files)} file(s) valid") | |
| if invalid_files and len(bru_files) <= 5: | |
| print("\nπ§ Fix suggestions:") | |
| for filename, issues in invalid_files: | |
| print(f"\n{filename}:") | |
| for issue in issues: | |
| print(f" β’ {issue}") | |
| return valid_count == len(bru_files) | |
| if __name__ == "__main__": | |
| success = main() | |
| sys.exit(0 if success else 1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment