From 19aa76744743664134b19f37f67d5d42039758a7 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Mon, 2 Feb 2026 06:20:57 +0000 Subject: [PATCH] Add CLI module with explain, generate, from-english, build commands --- regex_humanizer/cli.py | 331 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 regex_humanizer/cli.py diff --git a/regex_humanizer/cli.py b/regex_humanizer/cli.py new file mode 100644 index 0000000..935f33f --- /dev/null +++ b/regex_humanizer/cli.py @@ -0,0 +1,331 @@ +"""Main CLI module for Regex Humanizer.""" + +import json as json_module + +import click + +from .parser import parse_regex, ParseError +from .converter import convert_to_english, convert_to_english_verbose +from .examples import generate_examples +from .flavors import ( + get_supported_flavors, + validate_flavor, + detect_flavor, + get_compatibility_warnings, +) +from .converter.english_to_regex import convert_english_to_regex + + +@click.group(help="Regex Humanizer - Convert regular expressions into human-readable English") +@click.version_option(version="0.1.0") +def main(): + """A CLI tool that converts regular expressions into human-readable English.""" + pass + + +@main.command() +@click.argument("pattern", type=click.STRING) +@click.option( + "--flavor", + type=click.Choice(["pcre", "javascript", "python", "go"]), + default="pcre", + help="Regex flavor to use for parsing", +) +@click.option( + "--verbose/--simple", + default=False, + help="Show detailed breakdown of the pattern", +) +@click.option( + "--output-format", + type=click.Choice(["text", "json"]), + default="text", + help="Output format (text or json)", +) +@click.option( + "--json", + "output_format", + flag_value="json", + help="Output in JSON format", +) +def explain(pattern: str, flavor: str, verbose: bool, output_format: str): + """Explain a regex pattern in plain English.""" + use_json = output_format == "json" + + if not validate_flavor(flavor): + flavors = get_supported_flavors() + click.echo(f"Error: Unknown flavor '{flavor}'. Supported: {', '.join(flavors)}", err=True) + ctx = click.get_current_context() + ctx.exit(2) + + try: + if verbose or use_json: + result = convert_to_english_verbose(pattern, flavor) + if "Error" in result.get('description', ''): + click.echo(result['description'], err=True) + ctx = click.get_current_context() + ctx.exit(2) + if use_json: + click.echo(json_module.dumps(result, indent=2)) + else: + click.echo(f"\nPattern: {result['pattern']}") + click.echo(f"Flavor: {result['flavor']}") + click.echo(f"\nDescription:\n{result['description']}") + if result.get('structure'): + click.echo(f"\nStructure:") + for item in result['structure']: + click.echo(f" - {item}") + else: + description = convert_to_english(pattern, flavor) + if "Error" in description: + click.echo(description, err=True) + ctx = click.get_current_context() + ctx.exit(2) + click.echo(f"\nPattern: {pattern}") + click.echo(f"Flavor: {flavor}") + click.echo(f"\nDescription:\n{description}") + + warnings = get_compatibility_warnings(pattern, flavor) + if warnings: + click.echo(f"\nCompatibility warnings:") + for w in warnings: + click.echo(f" [{w.severity.upper()}] {w.feature}: {w.message}") + + except ParseError as e: + click.echo(f"Error parsing pattern: {e.message} at position {e.position}", err=True) + ctx = click.get_current_context() + ctx.exit(2) + except Exception as e: + click.echo(f"Error: {str(e)}", err=True) + ctx = click.get_current_context() + ctx.exit(2) + + +@main.command() +@click.argument("pattern", type=click.STRING) +@click.option( + "--flavor", + type=click.Choice(["pcre", "javascript", "python", "go"]), + default="pcre", + help="Regex flavor to use for parsing", +) +@click.option( + "--count", + "-n", + default=5, + type=click.IntRange(1, 20), + help="Number of examples to generate", +) +@click.option( + "--output-format", + type=click.Choice(["text", "json"]), + default="text", + help="Output format (text or json)", +) +@click.option( + "--json", + "output_format", + flag_value="json", + help="Output in JSON format", +) +def generate(pattern: str, flavor: str, count: int, output_format: str): + """Generate example strings that match the regex pattern.""" + use_json = output_format == "json" + + if not validate_flavor(flavor): + click.echo(f"Error: Unknown flavor '{flavor}'", err=True) + ctx = click.get_current_context() + ctx.exit(2) + + try: + examples = generate_examples(pattern, count, flavor) + + if use_json: + result = { + "pattern": pattern, + "flavor": flavor, + "examples": examples, + } + click.echo(json_module.dumps(result, indent=2)) + else: + click.echo(f"\nPattern: {pattern}") + click.echo(f"Flavor: {flavor}") + click.echo(f"\nMatching examples:") + for i, example in enumerate(examples, 1): + click.echo(f" {i}. {example}") + + if not examples: + click.echo(" No examples could be generated.") + + except Exception as e: + click.echo(f"Error: {str(e)}", err=True) + ctx = click.get_current_context() + ctx.exit(2) + + +@main.command() +@click.argument("description", type=click.STRING) +@click.option( + "--flavor", + type=click.Choice(["pcre", "javascript", "python", "go"]), + default="pcre", + help="Regex flavor to generate", +) +@click.option( + "--output-format", + type=click.Choice(["text", "json"]), + default="text", + help="Output format (text or json)", +) +@click.option( + "--json", + "output_format", + flag_value="json", + help="Output in JSON format", +) +def from_english(description: str, flavor: str, output_format: str): + """Convert an English description to a regex pattern.""" + use_json = output_format == "json" + + try: + result = convert_english_to_regex(description, flavor) + + if use_json: + click.echo(json_module.dumps(result, indent=2)) + else: + click.echo(f"\nEnglish description: {result['input']}") + click.echo(f"Generated pattern: {result['output']}") + click.echo(f"Flavor: {result['flavor']}") + + if result.get('warnings'): + click.echo(f"\nWarnings:") + for w in result['warnings']: + click.echo(f" - {w}") + + if result.get('valid') is False: + click.echo(f"\nValidation error: {result.get('error')}") + ctx = click.get_current_context() + ctx.exit(2) + + except Exception as e: + click.echo(f"Error: {str(e)}", err=True) + ctx = click.get_current_context() + ctx.exit(2) + + +@main.command() +@click.option( + "--flavor", + type=click.Choice(["pcre", "javascript", "python", "go"]), + default="pcre", + help="Regex flavor to use", +) +def build(flavor: str): + """Interactive wizard to build a regex pattern step by step.""" + click.echo("\n=== Regex Pattern Builder ===") + click.echo(f"Flavor: {flavor}") + click.echo("Enter 'quit' to exit, 'back' to go back, 'done' when finished.\n") + + pattern_parts = [] + + while True: + current_pattern = "".join(p.to_regex() if hasattr(p, 'to_regex') else str(p) for p in pattern_parts) + if current_pattern: + description = convert_to_english(current_pattern, flavor) + click.echo(f"\nCurrent pattern: {current_pattern}") + click.echo(f"Meaning: {description}") + else: + click.echo("\nCurrent pattern: (empty)") + + click.echo("\nWhat would you like to add?") + click.echo(" 1. Literal text") + click.echo(" 2. Character class (e.g., [abc])") + click.echo(" 3. Character range (e.g., [a-z])") + click.echo(" 4. Digit (\\d)") + click.echo(" 5. Word character (\\w)") + click.echo(" 6. Whitespace (\\s)") + click.echo(" 7. Any character (.)") + click.echo(" 8. Start of string (^)") + click.echo(" 9. End of string ($)") + click.echo(" 10. Word boundary (\\b)") + + choice = click.prompt("Enter your choice (1-10, quit/back/done)", type=str).strip().lower() + + if choice in ("quit", "exit", "q"): + click.echo("Goodbye!") + break + elif choice in ("back", "b"): + if pattern_parts: + pattern_parts.pop() + click.echo("Removed last element.") + else: + click.echo("Pattern is already empty.") + continue + elif choice in ("done", "finish", "d"): + if current_pattern: + click.echo(f"\nFinal pattern: {current_pattern}") + description = convert_to_english(current_pattern, flavor) + click.echo(f"Meaning: {description}") + examples = generate_examples(current_pattern, 3, flavor) + if examples: + click.echo(f"Examples: {', '.join(examples)}") + else: + click.echo("Pattern is empty. Nothing to save.") + break + elif choice == "1": + text = click.prompt("Enter the literal text", type=str) + if text: + from .parser import Literal + pattern_parts.append(Literal(value=text)) + elif choice == "2": + chars = click.prompt("Enter characters (e.g., 'abc')", type=str) + if chars: + from .parser import CharacterClass + pattern_parts.append(CharacterClass(characters=list(chars))) + elif choice == "3": + start = click.prompt("Start character (e.g., a)", type=str) + end = click.prompt("End character (e.g., z)", type=str) + if start and end: + from .parser import CharacterClass + pattern_parts.append(CharacterClass(ranges=[(start, end)])) + elif choice == "4": + from .parser import SpecialSequence + pattern_parts.append(SpecialSequence(sequence=r"\d")) + elif choice == "5": + from .parser import SpecialSequence + pattern_parts.append(SpecialSequence(sequence=r"\w")) + elif choice == "6": + from .parser import SpecialSequence + pattern_parts.append(SpecialSequence(sequence=r"\s")) + elif choice == "7": + from .parser import SpecialSequence + pattern_parts.append(SpecialSequence(sequence=".")) + elif choice == "8": + from .parser import Anchor + pattern_parts.append(Anchor(kind="^")) + elif choice == "9": + from .parser import Anchor + pattern_parts.append(Anchor(kind="$")) + elif choice == "10": + from .parser import Anchor + pattern_parts.append(Anchor(kind=r"\b")) + else: + click.echo("Invalid choice. Please enter 1-10 or a command.") + + +@main.command() +def flavors(): + """List supported regex flavors.""" + supported = get_supported_flavors() + click.echo("\nSupported regex flavors:") + for flavor in supported: + click.echo(f" - {flavor}") + click.echo() + + +@main.command() +@click.argument("pattern", type=click.STRING) +def detect(pattern: str): + """Detect the flavor of a regex pattern.""" + flavor = detect_flavor(pattern) + click.echo(f"\nDetected flavor: {flavor}")