"""Render the repository's terminal aliases as a reference page.
The ``aliases`` file is the source. An alias that only wraps a make target
carries no comment by repository rule, so its description is taken from the
target it calls; every other alias documents itself in the trailing comment.
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
from . import literal
ALIAS = re.compile(
r"^alias\s+([^=]+)=(['\"])(?P
.*)\2(?:\s*#\s*(?P.*\S))?\s*$"
)
MAKE_CALL = re.compile(r"(?:^|[\s;&(])(?:m|make)\s+([a-z][a-z0-9-]*)")
HEADER = """\
Terminal Aliases
================
The ``i8*`` shell aliases installed by ``make install-alias``, read from the
repository's ``aliases`` file. An alias that only wraps a make target is listed
with that target; the rest carry their own description. Generated by the
documentation build; edit ``aliases`` rather than this page.
"""
def parse(aliases_file: Path) -> list[tuple[str, str, str | None]]:
"""Return every alias as ``(name, body, comment)``.
Args:
aliases_file: the ``aliases`` file to read.
Raises:
ValueError: a line starts with ``alias`` but does not parse, which
would otherwise drop it from the page without a trace.
"""
parsed: list[tuple[str, str, str | None]] = []
for line in aliases_file.read_text(encoding="utf-8").splitlines():
if not line.startswith("alias "):
continue
match = ALIAS.match(line)
if match is None:
raise ValueError(f"unparseable alias line: {line!r}")
parsed.append(
(match.group(1).strip(), match.group("body"), match.group("comment"))
)
return parsed
def _entry(name: str, body: str, comment: str | None) -> str:
"""Return one definition-list entry for an alias.
Args:
name: the alias name.
body: what the alias expands to.
comment: the trailing comment, absent for a plain make wrapper.
"""
targets = sorted(set(MAKE_CALL.findall(body)))
if comment:
description = literal(comment)
elif targets:
description = "Runs " + ", ".join(f"``make {t}``" for t in targets) + "."
else:
description = "Undocumented."
return f"``{name}``\n {description}"
def generate(aliases_file: Path, output_file: Path) -> int:
"""Write the alias reference page.
Args:
aliases_file: the ``aliases`` file to read.
output_file: the ``.rst`` page to write.
Returns:
How many aliases the page documents.
"""
entries = parse(aliases_file)
output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_text(
HEADER + "\n\n".join(_entry(*entry) for entry in entries) + "\n",
encoding="utf-8",
)
return len(entries)
def main(argv=None):
parser = argparse.ArgumentParser(
description="Generate the alias reference from the aliases file."
)
parser.add_argument("--aliases-file", required=True, help="Path to aliases.")
parser.add_argument("--output-file", required=True, help="Path to the .rst page.")
args = parser.parse_args(argv)
output = Path(args.output_file)
count = generate(Path(args.aliases_file), output)
print(f"{count} alias(es) written to {output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())