Add CLI module with explain, generate, from-english, build commands
Some checks failed
CI / test (push) Failing after 12s
CI / build (push) Has been skipped

This commit is contained in:
2026-02-02 06:20:57 +00:00
parent 42dbbd8118
commit 19aa767447

331
regex_humanizer/cli.py Normal file
View 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}")