tldr/scripts/update-command.py

274 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""
A Python script to update the common contents of a command example across all languages.
Usage:
python3 scripts/update-command.py [-c] [-u] [-n] <PLATFORM> <FILENAME>
Options:
-c, --common-part COMMON_PART
Specify the common part to be modified (any content between double brackets will be ignored).
-u, --updated-common-part UPDATED_COMMON_PART
Specify the updated common part (any content between double brackets will be ignored).
-n, --dry-run
Show what changes would be made without actually modifying the page.
Examples:
1. Update 'cargo' page interactively:
python3 scripts/update-command.py common cargo
Enter the command examples (any content between double curly brackets will be ignored):
Enter the common part to modify: cargo search {{}}
Enter the change to be made: cargo search --limit 1 {{}}
2. Show what changes would be made by updating `sudo apt install {{}}` in 'apt' page to `sudo apt install {{}} --no-confirm`:
python3 scripts/update-command.py --dry-run -c "sudo apt install {{}}" -u "sudo apt install {{}} --no-confirm" linux apt
"""
from pathlib import Path
import os
import re
import argparse
import sys
from functools import reduce
import logging
class MyFormatter(logging.Formatter):
grey = "\x1b[0;30m"
yellow = "\x1b[33;20m"
red = "\x1b[31;20m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
format = "%(levelname)s: %(message)s (%(filename)s:%(lineno)d)"
FORMATS = {
logging.INFO: grey + format + reset,
logging.WARNING: yellow + format + reset,
logging.ERROR: red + format + reset,
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
logger = logging.getLogger(__name__)
logger.propagate = False
ch = logging.StreamHandler()
ch.setFormatter(MyFormatter())
logger.addHandler(ch)
def get_locales(base_path: Path) -> list[str]:
return [
d.name.split(".")[1]
for d in base_path.iterdir()
if d.is_dir() and d.name.startswith("pages.")
]
def take_cmd_example_with_common_part(cmd_examples: list[str], common_part: str) -> str:
return next(
(
f"`{cmd_example}`"
for cmd_example in cmd_examples
if remove_placeholders(cmd_example) == common_part
),
None,
)
def get_cmd_examples_of_page(page_text: str) -> list[str]:
command_pattern = re.compile(r"`([^`]+)`")
return re.findall(command_pattern, page_text)
def find_cmd_example_with_common_part(common_part: str, page_text: str) -> list[str]:
cmd_examples = get_cmd_examples_of_page(page_text)
return take_cmd_example_with_common_part(cmd_examples, common_part)
def get_page_path(tldr_root: Path, locale: str, platform: str, filename: str):
if locale == "":
return tldr_root / "pages" / platform / filename
return tldr_root / f"pages.{locale}" / platform / filename
def split_by_curly_brackets(s: str) -> list[str]:
return re.split(r"(\{\{.*?\}\})", s)
def parse_placeholders(cmd_example: str) -> list[str]:
return [
part.strip("{}")
for part in split_by_curly_brackets(cmd_example)
if part.startswith("{{") and part.endswith("}}")
]
def place_placeholders(cmd_example: str, placeholders: list[str]) -> str:
return reduce(
lambda cmd, ph: cmd.replace("{{}}", "{{" + ph + "}}", 1),
placeholders,
cmd_example,
)
def remove_placeholders(cmd_example: str) -> str:
return re.sub(r"\{\{.*?\}\}", "{{}}", cmd_example)
def add_backticks(cmd_example: str) -> str:
return "`" + cmd_example.strip("`") + "`"
def update_page(
page_path: Path,
old_common_part: str,
new_common_part: str,
dry_run: bool,
) -> None:
with page_path.open("r", encoding="utf-8") as file:
page_text = file.read()
logger.info(f"Processing page: {page_path}")
cmd_example = find_cmd_example_with_common_part(old_common_part, page_text)
if not cmd_example:
logger.warning(f"Common part '{old_common_part}' not found in '{page_path}'.")
return False
logger.info(f"Found command example: {cmd_example}")
new_cmd_example = add_backticks(
place_placeholders(new_common_part, parse_placeholders(cmd_example))
)
logger.info(f"{cmd_example} -> {new_cmd_example}")
if not dry_run:
new_page_text = page_text.replace(cmd_example, new_cmd_example)
with page_path.open("w", encoding="utf-8") as file:
file.write(new_page_text)
return True
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Update tldr pages.")
parser.add_argument(
"platform", help="Relative path to the page from the repository root"
)
parser.add_argument("filename", help="Page file name (without .md)")
parser.add_argument(
"-c", "--common-part", help="Common part to be modified", required=False
)
parser.add_argument(
"-u", "--updated-common-part", help="Updated common part", required=False
)
parser.add_argument(
"-n",
"--dry-run",
action="store_true",
help="Show what changes would be made without actually modifying the pages",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="Increase verbosity level (use -v, -vv)",
)
args = parser.parse_args()
if args.verbose > 0:
log_levels = [logging.WARNING, logging.INFO]
log_level = log_levels[min(args.verbose, len(log_levels) - 1)]
else:
log_level = logging.ERROR
logging.basicConfig(level=log_level)
return args
def update_pages(
tldr_root: str,
platform: str,
filename: str,
locales: list[str],
old_common_part: str,
updated_common_part: str,
dry_run: bool,
) -> None:
for locale in locales:
page_path = get_page_path(tldr_root, locale, platform, filename)
if page_path.exists() and page_path.is_file():
exists = update_page(
page_path,
old_common_part,
updated_common_part,
dry_run,
)
if not exists and locale == "":
logger.warning(
f"Common part '{old_common_part}' not found in '{page_path}'."
)
def clean_cmd_example(cmd_example: str) -> str:
return remove_placeholders(cmd_example).strip("`")
def get_tldr_root() -> Path:
f = Path("update-command.py").resolve()
return next(path for path in f.parents if path.name == "tldr")
if "TLDR_ROOT" in os.environ:
return Path(os.environ["TLDR_ROOT"])
logger.error(
"Please set TLDR_ROOT to the location of a clone of https://github.com/tldr-pages/tldr."
)
sys.exit(1)
def main():
args = parse_arguments()
print(
"Enter the command examples (any content between double curly brackets will be ignored):"
)
common_part = (
args.common_part
if args.common_part
else clean_cmd_example(input("Enter the common part to modify: "))
)
updated_common_part = (
args.updated_common_part
if args.updated_common_part
else clean_cmd_example(input("Enter the change to be made: "))
)
tldr_root = get_tldr_root()
locales = [""]
locales.extend(get_locales(tldr_root))
update_pages(
tldr_root,
args.platform,
args.filename + ".md",
locales,
common_part,
updated_common_part,
args.dry_run,
)
if __name__ == "__main__":
main()