From 9caf7d9061b35455544e6db75bb9941b27ced5b3 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sun, 1 Feb 2026 22:26:08 +0000 Subject: [PATCH] Initial upload: config-converter-cli v1.0.0 --- configconverter/cli.py | 395 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 configconverter/cli.py diff --git a/configconverter/cli.py b/configconverter/cli.py new file mode 100644 index 0000000..0e5aa9b --- /dev/null +++ b/configconverter/cli.py @@ -0,0 +1,395 @@ +"""CLI interface for config-converter-cli.""" + +import sys +from typing import List, Optional + +import click +from rich.console import Console + +from configconverter.converters import Converter +from configconverter.exceptions import ( + ConfigConverterError, + ParseError, + QueryError, + ValidationError, +) +from configconverter.formatters import Formatter +from configconverter.query import QueryEngine +from configconverter.validators import Validator + + +console = Console() +converter = Converter() +formatter = Formatter() +query_engine = QueryEngine() +validator = Validator() + + +@click.group() +@click.version_option(version="1.0.0") +def main(): + """A CLI tool for converting and validating configuration files.""" + pass + + +@main.command("convert") +@click.argument( + "input_file", + type=click.File("r"), + required=False, +) +@click.option( + "--from", + "-f", + "from_format", + type=click.Choice(["json", "yaml", "toml"]), + help="Input format (auto-detected if not specified)", +) +@click.option( + "--to", + "-t", + "to_format", + type=click.Choice(["json", "yaml", "toml"]), + required=True, + help="Output format", +) +@click.option( + "--indent", + "-i", + type=click.Choice(["2", "4", "8"]), + default="2", + help="Indentation level for output", +) +@click.option( + "--output", + "-o", + type=click.File("w"), + help="Output file (stdout if not specified)", +) +@click.option( + "--validate/--no-validate", + "-V", + default=True, + help="Validate input before conversion", +) +def convert( + input_file, + from_format: Optional[str], + to_format: str, + indent: str, + output, + validate: bool, +): + """Convert a configuration file from one format to another. + + INPUT_FILE can be a file path or '-' for stdin. + + Examples: + convert config.json --to yaml -o config.yaml + convert config.yaml --from yaml --to toml + cat config.json | convert - --to yaml + """ + indent_val = int(indent) + + try: + if input_file is None or input_file.name == "-": + content = click.get_text_stream("stdin").read() + else: + content = input_file.read() + + if not content.strip(): + if output: + output.write("") + return + + if validate: + is_valid, error = validator.validate(content, from_format) + if not is_valid: + console.print(f"[bold red]Validation Error:[/bold red] {error.message}") + if error.line_number: + console.print(f"Line {error.line_number}") + sys.exit(1) + + result = converter.convert(content, from_format or "auto", to_format) + + if output: + output.write(result) + else: + console.print(result) + + except ConfigConverterError as e: + console.print(f"[bold red]Error:[/bold red] {e.message}") + sys.exit(1) + except Exception as e: + console.print(f"[bold red]Unexpected Error:[/bold red] {e}") + sys.exit(1) + + +@main.command("validate") +@click.argument( + "input_file", + type=click.File("r"), + required=False, +) +@click.option( + "--format", + "-F", + "format_", + type=click.Choice(["json", "yaml", "toml"]), + help="Format hint (auto-detected if not specified)", +) +@click.option( + "--quiet", + "-q", + is_flag=True, + default=False, + help="Only return exit code, no output", +) +def validate(input_file, format_: Optional[str], quiet: bool): + """Validate the syntax of a configuration file. + + INPUT_FILE can be a file path or '-' for stdin. + + Examples: + validate config.json + validate config.yaml --format yaml + cat config.json | validate - + """ + try: + if input_file is None or input_file.name == "-": + content = click.get_text_stream("stdin").read() + else: + content = input_file.read() + + if not content.strip(): + if not quiet: + console.print("[bold green]Valid (empty)[/bold green]") + return + + is_valid, error = validator.validate(content, format_) + + if is_valid: + if not quiet: + detected = converter.detect_format(content) + console.print(f"[bold green]Valid {detected.upper()}[/bold green]") + else: + if not quiet: + validator.print_error(error, content, input_file.name if input_file else None) + sys.exit(1) + + except Exception as e: + if not quiet: + console.print(f"[bold red]Error:[/bold red] {e}") + sys.exit(1) + + +@main.command("query") +@click.argument("expression", type=str) +@click.argument( + "input_file", + type=click.File("r"), + required=False, +) +@click.option( + "--format", + "-F", + "format_", + type=click.Choice(["json", "yaml", "toml"]), + help="Format hint (auto-detected if not specified)", +) +@click.option( + "--output", + "-o", + type=click.Choice(["json", "text"]), + default="json", + help="Output format", +) +def query(expression: str, input_file, format_: Optional[str], output: str): + """Query configuration data using JMESPath. + + EXPRESSION is a JMESPath query expression. + + INPUT_FILE can be a file path or '-' for stdin. + + Examples: + query "server.host" config.json + query "services[*].name" config.yaml + cat config.json | query "database.settings.port" - + """ + import json + + try: + if input_file is None or (hasattr(input_file, 'name') and input_file.name == "-"): + content = click.get_text_stream("stdin").read() + elif input_file is not None: + content = input_file.read() + else: + content = click.get_text_stream("stdin").read() + + if not content.strip(): + console.print("null") + return + + result = query_engine.query(content, expression, format_) + + if result is None: + if output == "json": + console.print("null") + else: + console.print("(no match)") + else: + if output == "json": + console.print(json.dumps(result, indent=2, ensure_ascii=False)) + else: + console.print(str(result)) + + except QueryError as e: + console.print(f"[bold red]Query Error:[/bold red] {e.message}") + sys.exit(1) + except Exception as e: + console.print(f"[bold red]Error:[/bold red] {e}") + sys.exit(1) + + +@main.command("batch") +@click.argument( + "input_files", + type=click.File("r"), + nargs=-1, + required=True, +) +@click.option( + "--from", + "-f", + "from_format", + type=click.Choice(["json", "yaml", "toml"]), + help="Input format (auto-detected if not specified)", +) +@click.option( + "--to", + "-t", + "to_format", + type=click.Choice(["json", "yaml", "toml"]), + required=True, + help="Output format", +) +@click.option( + "--indent", + "-i", + type=click.Choice(["2", "4", "8"]), + default="2", + help="Indentation level for output", +) +@click.option( + "--output-dir", + "-o", + type=click.Path(exists=False, file_okay=False, dir_okay=True), + help="Output directory for converted files", +) +def batch( + input_files, + from_format: Optional[str], + to_format: str, + indent: str, + output_dir, +): + """Convert multiple files in batch mode. + + INPUT_FILES is one or more file paths. + + Examples: + batch *.json --to yaml --output-dir converted/ + batch config1.yaml config2.yaml --to toml + """ + from pathlib import Path + + indent_val = int(indent) + files = list(input_files) + + if not files: + console.print("[bold red]No files specified[/bold red]") + sys.exit(1) + + output_path = Path(output_dir) if output_dir else None + if output_path: + output_path.mkdir(parents=True, exist_ok=True) + + for input_file in files: + try: + content = input_file.read() + + if not content.strip(): + continue + + result = converter.convert(content, from_format or "auto", to_format) + + if output_dir: + output_file = output_path / (Path(input_file.name).stem + f".{to_format}") + with open(output_file, "w") as f: + f.write(result) + console.print(f"[cyan]{input_file.name}[/cyan] -> [green]{output_file.name}[/green]") + else: + console.print(f"[bold]=== {input_file.name} ===[/bold]") + console.print(result) + console.print() + + except Exception as e: + console.print(f"[bold red]Error processing {input_file.name}:[/bold red] {e}") + + +@main.command("format") +@click.argument( + "input_file", + type=click.File("r"), + required=False, +) +@click.option( + "--indent", + "-i", + type=click.Choice(["2", "4", "8"]), + default="2", + help="Indentation level", +) +@click.option( + "--to", + "-t", + "to_format", + type=click.Choice(["json", "yaml", "toml"]), + help="Output format (same as input if not specified)", +) +@click.option( + "--compact", + "-c", + is_flag=True, + default=False, + help="Use compact output", +) +def format_cmd(input_file, indent: str, to_format: Optional[str], compact: bool): + """Format/pretty-print a configuration file. + + INPUT_FILE can be a file path or '-' for stdin. + + Examples: + format config.json --indent 4 + format config.yaml --to json --compact + cat config.json | format - --indent 2 + """ + try: + if input_file is None or input_file.name == "-": + content = click.get_text_stream("stdin").read() + else: + content = input_file.read() + + if not content.strip(): + return + + result = formatter.format(content, indent=int(indent), compact=compact, format=to_format) + console.print(result) + + except Exception as e: + console.print(f"[bold red]Error:[/bold red] {e}") + sys.exit(1) + + +if __name__ == "__main__": + main()