Files
Giacomo Vercesi 591107cdce Overhaul PTML functionality
The logic of the `revng ptml` command was too tightly coupled with the
parsing of the xml. Overhaul the structure of the ptml code and split it
in two locations:
* `revng.ptml`: this module contains functions that allow easy
  manipulation of PTML, both for printing it and for obtaining the split
  metadata/text version.
* `revng.internal.cli._commands.ptml`: this implements the actual `revng
  ptml` command. This leverages the new logic in the `revng.ptml` module
  while maintaining the same functionality.
2025-05-05 17:28:48 +02:00

136 lines
4.9 KiB
Python

#
# This file is distributed under the MIT License. See LICENSE.md for details.
#
import argparse
import sys
from contextlib import contextmanager, suppress
from io import TextIOWrapper
from typing import IO, Generator, List, Optional, Union, cast
from revng.internal.cli.commands_registry import Command, CommandsRegistry, Options
from revng.internal.cli.support import file_wrapper
from revng.ptml.printer import ColorMode
from revng.support import to_bytes
from revng.support.artifacts import PTMLArtifact, ptml_artifact_autodetect
def normalize_filter_extract(filters: List[str], extract: Optional[str]) -> Union[str, List[str]]:
if extract is not None:
return extract
if len(filters) == 0:
return []
return ",".join(filters).split(",")
@contextmanager
def suppress_brokenpipe() -> Generator[None, None, None]:
"""When running a program with a pipe, BrokenPipeError might be raised. This signals that the
output pipe was closed, which we expect. Suppressing the exception is not enough since it can
also happen at shutdown, which will trigger python's unraisable hook, to remedy this we
overwrite the default hook to ignore BrokenPipeError."""
def new_unraisablehook(arg):
if arg.exc_type != BrokenPipeError:
sys.__unraisablehook__(arg)
old_unraisablehook = sys.unraisablehook
sys.unraisablehook = new_unraisablehook
with suppress(BrokenPipeError):
yield None
sys.unraisablehook = old_unraisablehook
def handler(args) -> int:
if args.inplace and args.input in (None, "-"):
sys.stderr.write("Cannot strip inplace while reading from stdin\n")
return 1
filters = normalize_filter_extract(args.filter, args.extract)
color = ColorMode.Autodetect
if args.color:
color = ColorMode.AutodetectForceColor
if args.plain:
color = ColorMode.Off
if args.inplace:
with open(args.input, "rb+") as input_file:
content = input_file.read()
input_file.seek(0)
input_file.truncate(0)
return handler_inner(content, TextIOWrapper(input_file, "utf-8"), color, filters)
else:
with file_wrapper(args.input, "rb") as input_file, file_wrapper(
args.output, "w"
) as output_file:
return handler_inner(input_file, output_file, color, filters)
def handler_inner(
content: bytes | IO[bytes], output: IO[str], color: ColorMode, filters: Union[str, List[str]]
):
with to_bytes(content) as wrapped:
artifact = ptml_artifact_autodetect(wrapped)
if isinstance(artifact, PTMLArtifact):
artifact.print(output, color)
return 0
if isinstance(filters, str):
artifact[filters].print(output, color)
elif len(filters) == 0:
artifact.print(output, color)
else:
artifact.print(output, color, lambda x: x in cast(List[str], filters))
return 0
class PTMLCommand(Command):
def __init__(self):
super().__init__(("ptml",), "Tool to manipulate PTML files")
def register_arguments(self, parser: argparse.ArgumentParser):
parser.description = "Tool to manipulate PTML files"
parser.add_argument("input", nargs="?", help="Input file (stdin if omitted)")
parser_format_group = parser.add_argument_group(
"Output Format",
"Picks the output format, if omitted it will be color on terminal\n"
"(if supported) or plain otherwise",
)
parser_format = parser_format_group.add_mutually_exclusive_group()
parser_format.add_argument("-p", "--plain", action="store_true", help="Plaintext output")
parser_format.add_argument("-c", "--color", action="store_true", help="Color output")
parser_filter_group = parser.add_argument_group("Output Filtering")
parser_filter = parser_filter_group.add_mutually_exclusive_group()
parser_filter.add_argument(
"-f",
"--filter",
type=str,
action="append",
default=[],
required=False,
help="Only show the specified comma-separated keys (if present)",
)
parser_filter.add_argument(
"-e", "--extract", type=str, required=False, help="Extract the specified key"
)
parser_out_group = parser.add_argument_group("Output")
parser_out = parser_out_group.add_mutually_exclusive_group()
parser_out.add_argument("-i", "--inplace", action="store_true", help="Strip inplace")
parser_out.add_argument(
"-o", "--output", nargs="?", metavar="FILE", help="Output file (stdout if omitted)"
)
def run(self, options: Options) -> Optional[int]:
with suppress(KeyboardInterrupt), suppress_brokenpipe():
return handler(options.parsed_args)
return 0
def setup(commands_registry: CommandsRegistry):
commands_registry.register_command(PTMLCommand())