Add CLI module with explain, generate, from-english, build commands
This commit is contained in:
331
regex_humanizer/cli.py
Normal file
331
regex_humanizer/cli.py
Normal file
@@ -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}")
|
||||
Reference in New Issue
Block a user