Browse Source

tooling: add update-command script (#11974)

* tooling: add update-command script

* update-command: add os import

Co-authored-by: K.B.Dharun Krishna <kbdharunkrishna@gmail.com>

* update-command: remove sync argument and small refactor

* update-command: add shebang and license

* update-command: remove unused function

* update-command: add dry-run option

* update-command: add docs in header

* update-command: fix old_common_part var name

* update-command: require command without .md

* update-command: fix command name in description

Co-authored-by: Juri Dispan <juri.dispan@posteo.net>

* update-command: remove ".md" suffix in description

* update-command: enclosing positional parameters with angle brackets

Co-authored-by: K.B.Dharun Krishna <kbdharunkrishna@gmail.com>

* update-command: enclose paremeters with double angle brackets

Co-authored-by: K.B.Dharun Krishna <kbdharunkrishna@gmail.com>

* update-command: refine wording

Co-authored-by: K.B.Dharun Krishna <kbdharunkrishna@gmail.com>

* update-command: remove unused functions

* update-command: detect tldr root from any subdir

* update-command: call place_placeholders directly

* update-command: add summary and compatibility to scripts/README.md

* scripts/update-command.py: remove unused method

* scripts/update-command.py: use logger.info instead of print

* scripts/update-command.py: rename command to cmd_example

* scripts/README.md: use command example instead of command

* scripts/update-command.py: remove nonexistent optional argument

* scripts/update-command.py: improve interactive example

* scripts/update-command.py: run black

---------

Co-authored-by: K.B.Dharun Krishna <kbdharunkrishna@gmail.com>
Co-authored-by: Juri Dispan <juri.dispan@posteo.net>
Co-authored-by: Sebastiaan Speck <12570668+sebastiaanspeck@users.noreply.github.com>
Vitor Henrique 1 year ago
parent
commit
561a364e0d
2 changed files with 275 additions and 0 deletions
  1. 2 0
      scripts/README.md
  2. 273 0
      scripts/update-command.py

+ 2 - 0
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) | ✅ | ✅ | ✅ |

+ 273 - 0
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] <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()