update-command.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. #!/usr/bin/env python3
  2. # SPDX-License-Identifier: MIT
  3. """
  4. A Python script to update the common contents of a command example across all languages.
  5. Usage:
  6. python3 scripts/update-command.py [-c] [-u] [-n] <PLATFORM> <FILENAME>
  7. Options:
  8. -c, --common-part COMMON_PART
  9. Specify the common part to be modified (any content between double brackets will be ignored).
  10. -u, --updated-common-part UPDATED_COMMON_PART
  11. Specify the updated common part (any content between double brackets will be ignored).
  12. -n, --dry-run
  13. Show what changes would be made without actually modifying the page.
  14. Examples:
  15. 1. Update 'cargo' page interactively:
  16. python3 scripts/update-command.py common cargo
  17. Enter the command examples (any content between double curly brackets will be ignored):
  18. Enter the common part to modify: cargo search {{}}
  19. Enter the change to be made: cargo search --limit 1 {{}}
  20. 2. Show what changes would be made by updating `sudo apt install {{}}` in 'apt' page to `sudo apt install {{}} --no-confirm`:
  21. python3 scripts/update-command.py --dry-run -c "sudo apt install {{}}" -u "sudo apt install {{}} --no-confirm" linux apt
  22. """
  23. from pathlib import Path
  24. import os
  25. import re
  26. import argparse
  27. import sys
  28. from functools import reduce
  29. import logging
  30. class MyFormatter(logging.Formatter):
  31. grey = "\x1b[0;30m"
  32. yellow = "\x1b[33;20m"
  33. red = "\x1b[31;20m"
  34. bold_red = "\x1b[31;1m"
  35. reset = "\x1b[0m"
  36. format = "%(levelname)s: %(message)s (%(filename)s:%(lineno)d)"
  37. FORMATS = {
  38. logging.INFO: grey + format + reset,
  39. logging.WARNING: yellow + format + reset,
  40. logging.ERROR: red + format + reset,
  41. }
  42. def format(self, record):
  43. log_fmt = self.FORMATS.get(record.levelno)
  44. formatter = logging.Formatter(log_fmt)
  45. return formatter.format(record)
  46. logger = logging.getLogger(__name__)
  47. logger.propagate = False
  48. ch = logging.StreamHandler()
  49. ch.setFormatter(MyFormatter())
  50. logger.addHandler(ch)
  51. def get_locales(base_path: Path) -> list[str]:
  52. return [
  53. d.name.split(".")[1]
  54. for d in base_path.iterdir()
  55. if d.is_dir() and d.name.startswith("pages.")
  56. ]
  57. def take_cmd_example_with_common_part(cmd_examples: list[str], common_part: str) -> str:
  58. return next(
  59. (
  60. f"`{cmd_example}`"
  61. for cmd_example in cmd_examples
  62. if remove_placeholders(cmd_example) == common_part
  63. ),
  64. None,
  65. )
  66. def get_cmd_examples_of_page(page_text: str) -> list[str]:
  67. command_pattern = re.compile(r"`([^`]+)`")
  68. return re.findall(command_pattern, page_text)
  69. def find_cmd_example_with_common_part(common_part: str, page_text: str) -> list[str]:
  70. cmd_examples = get_cmd_examples_of_page(page_text)
  71. return take_cmd_example_with_common_part(cmd_examples, common_part)
  72. def get_page_path(tldr_root: Path, locale: str, platform: str, filename: str):
  73. if locale == "":
  74. return tldr_root / "pages" / platform / filename
  75. return tldr_root / f"pages.{locale}" / platform / filename
  76. def split_by_curly_brackets(s: str) -> list[str]:
  77. return re.split(r"(\{\{.*?\}\})", s)
  78. def parse_placeholders(cmd_example: str) -> list[str]:
  79. return [
  80. part.strip("{}")
  81. for part in split_by_curly_brackets(cmd_example)
  82. if part.startswith("{{") and part.endswith("}}")
  83. ]
  84. def place_placeholders(cmd_example: str, placeholders: list[str]) -> str:
  85. return reduce(
  86. lambda cmd, ph: cmd.replace("{{}}", "{{" + ph + "}}", 1),
  87. placeholders,
  88. cmd_example,
  89. )
  90. def remove_placeholders(cmd_example: str) -> str:
  91. return re.sub(r"\{\{.*?\}\}", "{{}}", cmd_example)
  92. def add_backticks(cmd_example: str) -> str:
  93. return "`" + cmd_example.strip("`") + "`"
  94. def update_page(
  95. page_path: Path,
  96. old_common_part: str,
  97. new_common_part: str,
  98. dry_run: bool,
  99. ) -> None:
  100. with page_path.open("r", encoding="utf-8") as file:
  101. page_text = file.read()
  102. logger.info(f"Processing page: {page_path}")
  103. cmd_example = find_cmd_example_with_common_part(old_common_part, page_text)
  104. if not cmd_example:
  105. logger.warning(f"Common part '{old_common_part}' not found in '{page_path}'.")
  106. return False
  107. logger.info(f"Found command example: {cmd_example}")
  108. new_cmd_example = add_backticks(
  109. place_placeholders(new_common_part, parse_placeholders(cmd_example))
  110. )
  111. logger.info(f"{cmd_example} -> {new_cmd_example}")
  112. if not dry_run:
  113. new_page_text = page_text.replace(cmd_example, new_cmd_example)
  114. with page_path.open("w", encoding="utf-8") as file:
  115. file.write(new_page_text)
  116. return True
  117. def parse_arguments() -> argparse.Namespace:
  118. parser = argparse.ArgumentParser(description="Update tldr pages.")
  119. parser.add_argument(
  120. "platform", help="Relative path to the page from the repository root"
  121. )
  122. parser.add_argument("filename", help="Page file name (without .md)")
  123. parser.add_argument(
  124. "-c", "--common-part", help="Common part to be modified", required=False
  125. )
  126. parser.add_argument(
  127. "-u", "--updated-common-part", help="Updated common part", required=False
  128. )
  129. parser.add_argument(
  130. "-n",
  131. "--dry-run",
  132. action="store_true",
  133. help="Show what changes would be made without actually modifying the pages",
  134. )
  135. parser.add_argument(
  136. "-v",
  137. "--verbose",
  138. action="count",
  139. default=0,
  140. help="Increase verbosity level (use -v, -vv)",
  141. )
  142. args = parser.parse_args()
  143. if args.verbose > 0:
  144. log_levels = [logging.WARNING, logging.INFO]
  145. log_level = log_levels[min(args.verbose, len(log_levels) - 1)]
  146. else:
  147. log_level = logging.ERROR
  148. logging.basicConfig(level=log_level)
  149. return args
  150. def update_pages(
  151. tldr_root: str,
  152. platform: str,
  153. filename: str,
  154. locales: list[str],
  155. old_common_part: str,
  156. updated_common_part: str,
  157. dry_run: bool,
  158. ) -> None:
  159. for locale in locales:
  160. page_path = get_page_path(tldr_root, locale, platform, filename)
  161. if page_path.exists() and page_path.is_file():
  162. exists = update_page(
  163. page_path,
  164. old_common_part,
  165. updated_common_part,
  166. dry_run,
  167. )
  168. if not exists and locale == "":
  169. logger.warning(
  170. f"Common part '{old_common_part}' not found in '{page_path}'."
  171. )
  172. def clean_cmd_example(cmd_example: str) -> str:
  173. return remove_placeholders(cmd_example).strip("`")
  174. def get_tldr_root() -> Path:
  175. f = Path("update-command.py").resolve()
  176. return next(path for path in f.parents if path.name == "tldr")
  177. if "TLDR_ROOT" in os.environ:
  178. return Path(os.environ["TLDR_ROOT"])
  179. logger.error(
  180. "Please set TLDR_ROOT to the location of a clone of https://github.com/tldr-pages/tldr."
  181. )
  182. sys.exit(1)
  183. def main():
  184. args = parse_arguments()
  185. print(
  186. "Enter the command examples (any content between double curly brackets will be ignored):"
  187. )
  188. common_part = (
  189. args.common_part
  190. if args.common_part
  191. else clean_cmd_example(input("Enter the common part to modify: "))
  192. )
  193. updated_common_part = (
  194. args.updated_common_part
  195. if args.updated_common_part
  196. else clean_cmd_example(input("Enter the change to be made: "))
  197. )
  198. tldr_root = get_tldr_root()
  199. locales = [""]
  200. locales.extend(get_locales(tldr_root))
  201. update_pages(
  202. tldr_root,
  203. args.platform,
  204. args.filename + ".md",
  205. locales,
  206. common_part,
  207. updated_common_part,
  208. args.dry_run,
  209. )
  210. if __name__ == "__main__":
  211. main()