Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions doc/admin-guide/configuration/hrw4u.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,56 @@ follows to produce the help output:

hrw4u --help

Doing a compile is simply:
Basic Usage
^^^^^^^^^^^

Compile a single file to stdout:

.. code-block:: none

hrw4u some_file.hrw4u

in Addition to ``hrw4u``, you also have the reverse tool, converting existing ``header_rewrite``
configurations to ``hrw4u``. This tool is named ``u4wrh``. For people using IDEs, the package also
provides an LSP for this language, named ``hrw4u-lsp``.
Compile from stdin:

.. code-block:: none

cat some_file.hrw4u | hrw4u

Compile multiple files to stdout (separated by ``# ---``):

.. code-block:: none

hrw4u file1.hrw4u file2.hrw4u file3.hrw4u

Bulk Compilation
^^^^^^^^^^^^^^^^

For bulk compilation, use the ``input:output`` format to compile multiple files
to their respective output files in a single command:

.. code-block:: none

hrw4u file1.hrw4u:file1.conf file2.hrw4u:file2.conf file3.hrw4u:file3.conf

This is particularly useful for build systems or when processing many configuration
files at once. All files are processed in a single invocation, improving performance
for large batches of files.

Reverse Tool (u4wrh)
^^^^^^^^^^^^^^^^^^^^

In addition to ``hrw4u``, you also have the reverse tool, converting existing ``header_rewrite``
configurations to ``hrw4u``. This tool is named ``u4wrh`` and supports the same usage patterns:

.. code-block:: none

# Convert single file to stdout
u4wrh existing_config.conf

# Bulk conversion
u4wrh file1.conf:file1.hrw4u file2.conf:file2.hrw4u

For people using IDEs, the package also provides an LSP for this language, named ``hrw4u-lsp``.

Syntax Differences
==================
Expand Down
27 changes: 9 additions & 18 deletions tools/hrw4u/scripts/hrw4u
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,19 @@ from __future__ import annotations
from hrw4u.hrw4uLexer import hrw4uLexer
from hrw4u.hrw4uParser import hrw4uParser
from hrw4u.visitor import HRW4UVisitor
from hrw4u.common import create_base_parser, create_parse_tree, generate_output, process_input
from hrw4u.common import run_main


def main() -> None:
"""Main entry point for the hrw4u script."""
parser, output_group = create_base_parser("Process HRW4U input and produce output (AST or HRW).")

# Argument parsing
output_group.add_argument("--hrw", action="store_true", help="Produce the HRW output (default)")
parser.add_argument("--no-comments", action="store_true", help="Skip comment preservation (ignore comments in output)")
args = parser.parse_args()

# Default to HRW output if neither AST nor HRW specified
if not (args.ast or args.hrw):
args.hrw = True

content, filename = process_input(args.input_file)
tree, parser_obj, error_collector = create_parse_tree(
content, filename, hrw4uLexer, hrw4uParser, "hrw4u", not args.stop_on_error)

# Generate output
generate_output(tree, parser_obj, HRW4UVisitor, filename, args, error_collector)
run_main(
description="Process HRW4U input and produce output (AST or HRW).",
lexer_class=hrw4uLexer,
parser_class=hrw4uParser,
visitor_class=HRW4UVisitor,
error_prefix="hrw4u",
output_flag_name="hrw",
output_flag_help="Produce the HRW output (default)")


if __name__ == "__main__":
Expand Down
27 changes: 9 additions & 18 deletions tools/hrw4u/scripts/u4wrh
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,22 @@

from __future__ import annotations

from hrw4u.common import create_base_parser, create_parse_tree, generate_output, process_input
from hrw4u.common import run_main
from u4wrh.hrw_visitor import HRWInverseVisitor
from u4wrh.u4wrhLexer import u4wrhLexer
from u4wrh.u4wrhParser import u4wrhParser


def main() -> None:
"""Main entry point for the u4wrh script."""
parser, output_group = create_base_parser("Process header_rewrite (HRW) lines and reconstruct hrw4u source.")

# Argument parsing
output_group.add_argument("--hrw4u", action="store_true", help="Produce reconstructed hrw4u output (default)")
parser.add_argument("--no-comments", action="store_true", help="Skip comment preservation (ignore comments in output)")
args = parser.parse_args()

# Default to hrw4u output if neither AST nor hrw4u specified
if not (args.ast or args.hrw4u):
args.hrw4u = True

content, filename = process_input(args.input_file)
tree, parser_obj, error_collector = create_parse_tree(
content, filename, u4wrhLexer, u4wrhParser, "u4wrh", not args.stop_on_error)

# Generate output
generate_output(tree, parser_obj, HRWInverseVisitor, filename, args, error_collector)
run_main(
description="Process header_rewrite (HRW) lines and reconstruct hrw4u source.",
lexer_class=u4wrhLexer,
parser_class=u4wrhParser,
visitor_class=HRWInverseVisitor,
error_prefix="u4wrh",
output_flag_name="hrw4u",
output_flag_help="Produce reconstructed hrw4u output (default)")


if __name__ == "__main__":
Expand Down
104 changes: 104 additions & 0 deletions tools/hrw4u/src/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,107 @@ def generate_output(
print(error_collector.get_error_summary(), file=sys.stderr)
if not args.ast and tree is None:
sys.exit(1)


def run_main(
description: str, lexer_class: type[LexerProtocol], parser_class: type[ParserProtocol],
visitor_class: type[VisitorProtocol], error_prefix: str, output_flag_name: str, output_flag_help: str) -> None:
"""
Generic main function for hrw4u and u4wrh scripts with bulk compilation support.

Args:
description: Description for argument parser
lexer_class: ANTLR lexer class to use
parser_class: ANTLR parser class to use
visitor_class: Visitor class to use
error_prefix: Error prefix for error messages
output_flag_name: Name of output flag (e.g., "hrw", "hrw4u")
output_flag_help: Help text for output flag
"""
parser = argparse.ArgumentParser(
description=description,
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="For bulk compilation to files, use: input1.txt:output1.txt input2.txt:output2.txt ...")

parser.add_argument(
"files", help="Input file(s) to parse. Use input:output for bulk file output (default: stdin to stdout)", nargs="*")

output_group = parser.add_mutually_exclusive_group()
output_group.add_argument("--ast", action="store_true", help="Produce the ANTLR parse tree only")
output_group.add_argument(f"--{output_flag_name}", action="store_true", help=output_flag_help)

parser.add_argument("--no-comments", action="store_true", help="Skip comment preservation (ignore comments in output)")
parser.add_argument("--debug", action="store_true", help="Enable debug output")
parser.add_argument(
"--stop-on-error", action="store_true", help="Stop processing on first error (default: collect and report multiple errors)")

args = parser.parse_args()

if not hasattr(args, output_flag_name):
setattr(args, output_flag_name, False)

if not (args.ast or getattr(args, output_flag_name)):
setattr(args, output_flag_name, True)

if not args.files:
content, filename = process_input(sys.stdin)
tree, parser_obj, error_collector = create_parse_tree(
content, filename, lexer_class, parser_class, error_prefix, not args.stop_on_error)
generate_output(tree, parser_obj, visitor_class, filename, args, error_collector)
return

if any(':' in f for f in args.files):
for pair in args.files:
if ':' not in pair:
print(
f"Error: Mixed formats not allowed. All files must use 'input:output' format for bulk compilation.",
file=sys.stderr)
sys.exit(1)

input_path, output_path = pair.split(':', 1)

try:
with open(input_path, 'r', encoding='utf-8') as input_file:
content = input_file.read()
filename = input_path
except FileNotFoundError:
print(f"Error: Input file '{input_path}' not found", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error reading '{input_path}': {e}", file=sys.stderr)
sys.exit(1)

tree, parser_obj, error_collector = create_parse_tree(
content, filename, lexer_class, parser_class, error_prefix, not args.stop_on_error)

try:
with open(output_path, 'w', encoding='utf-8') as output_file:
original_stdout = sys.stdout
try:
sys.stdout = output_file
generate_output(tree, parser_obj, visitor_class, filename, args, error_collector)
finally:
sys.stdout = original_stdout
except Exception as e:
print(f"Error writing to '{output_path}': {e}", file=sys.stderr)
sys.exit(1)
else:
for i, input_path in enumerate(args.files):
if i > 0:
print("# ---")

try:
with open(input_path, 'r', encoding='utf-8') as input_file:
content = input_file.read()
filename = input_path
except FileNotFoundError:
print(f"Error: Input file '{input_path}' not found", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error reading '{input_path}': {e}", file=sys.stderr)
sys.exit(1)

tree, parser_obj, error_collector = create_parse_tree(
content, filename, lexer_class, parser_class, error_prefix, not args.stop_on_error)

generate_output(tree, parser_obj, visitor_class, filename, args, error_collector)
2 changes: 1 addition & 1 deletion tools/hrw4u/src/hrw_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __init__(
super().__init__(filename=filename, debug=debug, error_collector=error_collector)

# HRW inverse-specific state
self.section_label = section_label
self._section_label = section_label
self.preserve_comments = preserve_comments
self._pending_terms: list[tuple[str, CondState]] = []
self._in_group: bool = False
Expand Down
50 changes: 50 additions & 0 deletions tools/hrw4u/tests/test_bulk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations

import pytest
import utils


@pytest.mark.conds
def test_conds_bulk_compilation() -> None:
"""Test bulk compilation of all conds test cases."""
utils.run_bulk_test("conds")


@pytest.mark.examples
def test_examples_bulk_compilation() -> None:
"""Test bulk compilation of all examples test cases."""
utils.run_bulk_test("examples")


@pytest.mark.hooks
def test_hooks_bulk_compilation() -> None:
"""Test bulk compilation of all hooks test cases."""
utils.run_bulk_test("hooks")


@pytest.mark.ops
def test_ops_bulk_compilation() -> None:
"""Test bulk compilation of all ops test cases."""
utils.run_bulk_test("ops")


@pytest.mark.vars
def test_vars_bulk_compilation() -> None:
"""Test bulk compilation of all vars test cases."""
utils.run_bulk_test("vars")
Loading