diff --git a/scripts/README.md b/scripts/README.md index 9d3bee40f..b585127ac 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -16,6 +16,7 @@ This section contains a summary of the scripts available in this directory. For - [set-more-info-link.py](set-more-info-link.py) is a Python script to generate or update more information links across pages. - [test.sh](test.sh) script runs some basic tests on every PR/commit to make sure that the pages are valid and that the code is formatted correctly. - [wrong-filename.sh](wrong-filename.sh) script checks the consistency between the filenames and the page title. +- [update-command.py](update-command.py) is a Python script to update the common contents of a command example across all languages. ## Compatibility @@ -29,3 +30,4 @@ The below table shows the compatibility of user-executable scripts with differen | [set-alias-pages.py](set-alias-pages.py) | ✅ | ✅ | ✅ | | [set-more-info-link.py](set-more-info-link.py) | ✅ | ✅ | ✅ | | [wrong-filename.sh](wrong-filename.sh) | ✅ | ❌ | ❌ | +| [update-command.py](update-command.py) | ✅ | ✅ | ✅ | diff --git a/scripts/update-command.py b/scripts/update-command.py new file mode 100755 index 000000000..389f4d2b6 --- /dev/null +++ b/scripts/update-command.py @@ -0,0 +1,273 @@ +#!/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] + +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()