332 lines
11 KiB
Python
332 lines
11 KiB
Python
"""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}")
|