Files
regex-humanizer-cli/regex_humanizer/cli.py
7000pctAUTO 3292ca705e
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / release (push) Has been cancelled
Initial upload: regex-humanizer-cli with CI/CD workflow
2026-02-06 01:09:44 +00:00

281 lines
8.1 KiB
Python

"""Command-line interface for Regex Humanizer."""
import json
import sys
from typing import Optional
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, get_available_flavors
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(f"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()