import json import sys import click from .parser import parse_regex from .translator import translate_regex from .test_generator import generate_test_cases from .flavors import get_flavor_manager from .interactive import start_interactive_mode @click.group() @click.option( "--flavor", type=click.Choice(["pcre", "javascript", "python"]), default="pcre", help="Regex flavor to use", ) @click.pass_context def main(ctx: click.Context, flavor: str): """Regex Humanizer CLI - Convert regex patterns to human-readable English and generate test cases.""" ctx.ensure_object(dict) ctx.obj["flavor"] = flavor @main.command("explain") @click.argument("pattern", type=str) @click.option( "--output", "-o", type=click.Choice(["text", "json"]), default="text", help="Output format", ) @click.option( "--verbose", "-v", is_flag=True, help="Show detailed breakdown", ) @click.option( "--flavor", "-f", type=click.Choice(["pcre", "javascript", "python"]), default=None, help="Regex flavor to use", ) @click.pass_context def explain(ctx: click.Context, pattern: str, output: str, verbose: bool, flavor: str): """Explain a regex pattern in human-readable English.""" if ctx.obj is None: ctx.obj = {} flavor = flavor or ctx.obj.get("flavor", "pcre") try: ast = parse_regex(pattern, flavor) translation = translate_regex(pattern, flavor) if output == "json": result = { "pattern": pattern, "flavor": flavor, "explanation": translation, "verbose": { "node_count": len(get_all_nodes(ast)), "features": identify_features(ast), } if verbose else None, } click.echo(json.dumps(result, indent=2)) else: click.echo(f"\nPattern: {pattern}") click.echo(f"Flavor: {flavor}") click.echo("\nEnglish Explanation:") click.echo("-" * 50) click.echo(translation) click.echo() if verbose: features = identify_features(ast) click.echo("\nFeatures detected:") for feature in features: click.echo(f" - {feature}") except Exception as e: click.echo(f"Error: {e}", err=True) sys.exit(1) @main.command("test") @click.argument("pattern", type=str) @click.option( "--output", "-o", type=click.Choice(["text", "json"]), default="text", help="Output format", ) @click.option( "--count", "-n", type=int, default=5, help="Number of test cases to generate", ) @click.pass_context def test(ctx: click.Context, pattern: str, output: str, count: int): """Generate test cases (matching and non-matching) for a regex pattern.""" if ctx.obj is None: ctx.obj = {} flavor = ctx.obj.get("flavor", "pcre") try: result = generate_test_cases( pattern, flavor, matching_count=count, non_matching_count=count ) if output == "json": click.echo(json.dumps(result, indent=2)) else: click.echo(f"\nPattern: {pattern}") click.echo(f"Flavor: {flavor}") click.echo("\nMatching strings (should match the pattern):") click.echo("-" * 50) for i, s in enumerate(result["matching"], 1): click.echo(f" {i}. {s}") click.echo("\nNon-matching strings (should NOT match the pattern):") click.echo("-" * 50) for i, s in enumerate(result["non_matching"], 1): click.echo(f" {i}. {s}") click.echo() except Exception as e: click.echo(f"Error: {e}", err=True) sys.exit(1) @main.command("interactive") @click.option( "--flavor", "-f", type=click.Choice(["pcre", "javascript", "python"]), default="pcre", help="Regex flavor to use", ) @click.pass_context def interactive(ctx: click.Context, flavor: str): """Start an interactive REPL for exploring regex patterns.""" start_interactive_mode(flavor=flavor) @main.command("flavors") @click.pass_context def flavors(ctx: click.Context): """List available regex flavors.""" manager = get_flavor_manager() flavor_list = manager.list_flavors() click.echo("\nAvailable Regex Flavors:") click.echo("-" * 50) for name, desc in flavor_list: click.echo(f"\n {name}:") click.echo(f" {desc}") click.echo() @main.command("validate") @click.argument("pattern", type=str) @click.option( "--flavor", "-f", type=click.Choice(["pcre", "javascript", "python"]), default=None, help="Specific flavor to validate against", ) @click.pass_context def validate(ctx: click.Context, pattern: str, flavor: str): """Validate a regex pattern.""" if ctx.obj is None: ctx.obj = {} check_flavor = flavor or ctx.obj.get("flavor", "pcre") try: ast = parse_regex(pattern, check_flavor) click.echo(f"\nPattern: {pattern}") click.echo(f"Flavor: {check_flavor}") click.echo("\nValidation: PASSED") click.echo(f"AST node count: {len(get_all_nodes(ast))}") except Exception as e: click.echo(f"\nPattern: {pattern}") click.echo("Validation: FAILED") click.echo(f"Error: {e}") sys.exit(1) @main.command("convert") @click.argument("pattern", type=str) @click.option( "--from-flavor", "-s", type=click.Choice(["pcre", "javascript", "python"]), default="pcre", help="Source flavor", ) @click.option( "--to-flavor", "-t", type=click.Choice(["pcre", "javascript", "python"]), default="javascript", help="Target flavor", ) @click.pass_context def convert(ctx: click.Context, pattern: str, from_flavor: str, to_flavor: str): """Convert a regex pattern between flavors.""" manager = get_flavor_manager() converted, warnings = manager.convert(pattern, from_flavor, to_flavor) click.echo(f"\nOriginal ({from_flavor}): {pattern}") click.echo(f"Converted ({to_flavor}): {converted}") if warnings: click.echo("\nWarnings:") for warning in warnings: click.echo(f" - {warning}") def get_all_nodes(ast) -> list: """Get all nodes from AST.""" nodes = [ast] for child in getattr(ast, 'children', []): nodes.extend(get_all_nodes(child)) return nodes def identify_features(ast) -> list[str]: """Identify features in a regex pattern.""" features = [] nodes = get_all_nodes(ast) node_types = set(n.node_type.name for n in nodes) if "LOOKAHEAD" in node_types or "NEGATIVE_LOOKAHEAD" in node_types: features.append("Lookahead assertions") if "LOOKBEHIND" in node_types or "NEGATIVE_LOOKBEHIND" in node_types: features.append("Lookbehind assertions") if "NAMED_GROUP" in node_types: features.append("Named groups") if "CAPTURING_GROUP" in node_types: features.append("Capturing groups") if "NON_CAPTURING_GROUP" in node_types: features.append("Non-capturing groups") if "QUANTIFIER" in node_types: features.append("Quantifiers") for n in nodes: if n.node_type.name == "QUANTIFIER" and n.is_lazy: features.append("Lazy quantifiers") break if n.node_type.name == "QUANTIFIER" and n.is_possessive: features.append("Possessive quantifiers") break if "POSITIVE_SET" in node_types or "NEGATIVE_SET" in node_types: features.append("Character classes") if "ANCHOR_START" in node_types or "ANCHOR_END" in node_types: features.append("Anchors") if "DIGIT" in node_types or "WORD_CHAR" in node_types or "WHITESPACE" in node_types: features.append("Shorthand character classes") if "BACKREFERENCE" in node_types: features.append("Backreferences") return features if __name__ == "__main__": main()