mirror of
https://github.com/revng/revng
synced 2026-06-21 14:07:57 +00:00
591107cdce
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.
136 lines
4.9 KiB
Python
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())
|