From fa7cff98de7394209893726d031351107a99a62f Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Mon, 2 Feb 2026 20:50:56 +0000 Subject: [PATCH] Initial upload: Code Privacy Shield v0.1.0 --- src/code_privacy_shield/cli.py | 225 +++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 src/code_privacy_shield/cli.py diff --git a/src/code_privacy_shield/cli.py b/src/code_privacy_shield/cli.py new file mode 100644 index 0000000..a1de499 --- /dev/null +++ b/src/code_privacy_shield/cli.py @@ -0,0 +1,225 @@ +import sys +from pathlib import Path +from typing import List, Optional + +import click + +from . import __version__ +from .config import Config +from .redactor import Redactor +from .patterns import PatternLibrary + + +@click.group() +@click.version_option(version=__version__) +@click.option("--config", "-c", type=click.Path(), help="Path to config file") +@click.option("--quiet", "-q", is_flag=True, help="Suppress output") +@click.pass_context +def main(ctx: click.Context, config: str, quiet: bool) -> None: + ctx.ensure_object(dict) + ctx.obj["config_path"] = config + ctx.obj["quiet"] = quiet if quiet is not None else False + + +@main.command() +@click.argument("paths", nargs=-1, type=click.Path(exists=True)) +@click.option("--output", "-o", type=click.Path(), help="Output file (stdout if not specified)") +@click.option("--preview", "-p", is_flag=True, help="Preview what will be redacted") +@click.option("--in-place", "-i", is_flag=True, help="Edit files in place") +@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text", help="Output format") +@click.pass_context +def redact(ctx: click.Context, paths: tuple, output: str, preview: bool, in_place: bool, output_format: str) -> None: + quiet = ctx.obj.get("quiet", False) + config_path = ctx.obj.get("config_path") + + config = Config(config_path) + config.load() + + if preview: + config.set("general.preview_mode", True) + + pattern_library = PatternLibrary() + redactor = Redactor(pattern_library) + + for custom_pattern in config.get_custom_patterns(): + redactor.add_custom_pattern( + name=custom_pattern.get("name", "Custom"), + pattern=custom_pattern.get("pattern", ""), + category=custom_pattern.get("category", "custom"), + ) + + if not paths: + content = click.get_text_stream("stdin").read() + result = redactor.redact(content, preserve_structure=config.should_preserve_structure()) + + if preview: + _show_preview(result, output_format) + else: + click.echo(result.redacted) + return + + for path_str in paths: + path = Path(path_str) + + if path.is_file(): + _process_file(redactor, path, output, preview, in_place, output_format, config) + elif path.is_dir(): + _process_directory(redactor, path, output, preview, in_place, output_format, config) + + +def _process_file( + redactor: Redactor, + path: Path, + output: Optional[str], + preview: bool, + in_place: bool, + output_format: str, + config: Config +) -> None: + try: + result = redactor.redact_file(str(path), encoding="utf-8") + + if preview: + if not config.is_quiet_mode(): + click.echo(f"\n=== {path} ===") + _show_preview(result, output_format) + elif in_place: + path.write_text(result.redacted, encoding="utf-8") + if not config.is_quiet_mode(): + summary = redactor.get_match_summary(result) + click.echo(f"Redacted {path}: {len(result.matches)} matches found") + elif output: + Path(output).write_text(result.redacted, encoding="utf-8") + else: + click.echo(result.redacted) + + except (IOError, UnicodeDecodeError) as e: + click.echo(f"Error processing {path}: {e}", err=True) + + +def _process_directory( + redactor: Redactor, + path: Path, + output: Optional[str], + preview: bool, + in_place: bool, + output_format: str, + config: Config +) -> None: + exclude_patterns = config.get_exclude_patterns() + + for file_path in path.rglob("*"): + if file_path.is_file(): + if _should_exclude(file_path, exclude_patterns): + continue + + _process_file(redactor, file_path, output, preview, in_place, output_format, config) + + +def _should_exclude(file_path: Path, exclude_patterns: List[str]) -> bool: + for pattern in exclude_patterns: + if file_path.match(pattern): + return True + if pattern.startswith("*") and file_path.name.endswith(pattern[1:]): + return True + return False + + +def _show_preview(result, output_format: str) -> None: + if output_format == "json": + import json + preview_data = { + "matches": [ + { + "category": m.category, + "name": m.name, + "original": m.original, + "replacement": m.replacement, + } + for m in result.matches + ], + "total_matches": len(result.matches), + "categories": list(result.categories), + } + click.echo(json.dumps(preview_data, indent=2)) + else: + for match in result.matches: + click.echo(f"[{match.category}] {match.name}: {match.original} -> {match.replacement}") + + +@main.command() +@click.argument("path", type=click.Path()) +@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text", help="Output format") +@click.pass_context +def preview(ctx: click.Context, path: str, output_format: str) -> None: + ctx.invoke(redact, paths=(path,), preview=True, output_format=output_format) + + +@main.command() +@click.argument("path", type=click.Path()) +@click.pass_context +def check(ctx: click.Context, path: str) -> None: + config_path = ctx.obj.get("config_path") + + config = Config(config_path) + config.load() + + pattern_library = PatternLibrary() + redactor = Redactor(pattern_library) + + try: + if Path(path).is_file(): + result = redactor.redact_file(path, encoding="utf-8") + else: + content = Path(path).read_text(encoding="utf-8") + result = redactor.redact(content) + + if result.matches: + click.echo(f"Found {len(result.matches)} potential sensitive items:") + summary = redactor.get_match_summary(result) + for key, count in sorted(summary.items()): + click.echo(f" - {key}: {count}") + sys.exit(1) + else: + click.echo("No sensitive data detected.") + sys.exit(0) + + except (IOError, UnicodeDecodeError) as e: + click.echo(f"Error reading {path}: {e}", err=True) + sys.exit(1) + + +@main.command() +@click.argument("path", type=click.Path()) +@click.pass_context +def init_config(ctx: click.Context, path: str) -> None: + if Config.create_example_config(path): + click.echo(f"Created example config at {path}") + else: + click.echo("Failed to create config file", err=True) + sys.exit(1) + + +@main.command() +@click.pass_context +def config_locations(ctx: click.Context) -> None: + click.echo(f"Project config: {Config.get_project_config_path()}") + click.echo(f"User config: {Config.get_default_config_path()}") + + +@main.group() +def config() -> None: + """Manage configuration.""" + pass + + +@config.command("show") +@click.pass_context +def config_show(ctx: click.Context) -> None: + import toml + + config_path = ctx.obj.get("config_path") + config = Config(config_path) + config.load() + + click.echo(toml.dumps(config.config))