396 lines
9.9 KiB
Python
396 lines
9.9 KiB
Python
"""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()
|