Initial upload: config-converter-cli v1.0.0
This commit is contained in:
395
configconverter/cli.py
Normal file
395
configconverter/cli.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user