"""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()