set-alias-page.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. #!/usr/bin/env python3
  2. # SPDX-License-Identifier: MIT
  3. """
  4. A Python script to generate or update alias pages.
  5. Disclaimer: This script generates a lot of false positives so it isn't suggested to use the sync option. If used, only stage changes and commit verified changes for your language by using -l LANGUAGE.
  6. Note: If the current directory or one of its parents is called "tldr", the script will assume it is the tldr root, i.e., the directory that contains a clone of https://github.com/tldr-pages/tldr
  7. If you aren't, the script will use TLDR_ROOT as the tldr root. Also, ensure 'git' is available.
  8. Usage:
  9. python3 scripts/set-alias-page.py [-p PAGE] [-S] [-l LANGUAGE] [-s] [-n] [COMMAND]
  10. Options:
  11. -p, --page PAGE
  12. Specify the alias page in the format "platform/alias_command.md".
  13. -S, --sync
  14. Synchronize each translation's alias page (if exists) with that of the English page.
  15. -l, --language LANGUAGE
  16. Specify the language, a POSIX Locale Name in the form of "ll" or "ll_CC" (e.g. "fr" or "pt_BR").
  17. -s, --stage
  18. Stage modified pages (requires 'git' on $PATH and TLDR_ROOT to be a Git repository).
  19. -n, --dry-run
  20. Show what changes would be made without actually modifying the page.
  21. Positional Argument:
  22. COMMAND The command to be set as the alias command.
  23. Examples:
  24. 1. Add 'vi' as an alias page of 'vim':
  25. python3 scripts/set-alias-page.py -p common/vi vim
  26. python3 scripts/set-alias-page.py --page common/vi vim
  27. 2. Read English alias pages and synchronize them into all translations:
  28. python3 scripts/set-alias-page.py -S
  29. python3 scripts/set-alias-page.py --sync
  30. 3. Read English alias pages and synchronize them for Brazilian Portuguese pages only:
  31. python3 scripts/set-alias-page.py -S -l pt_BR
  32. python3 scripts/set-alias-page.py --sync --language pt_BR
  33. 4. Read English alias pages, synchronize them into all translations and stage modified pages for commit:
  34. python3 scripts/set-alias-page.py -Ss
  35. python3 scripts/set-alias-page.py --sync --stage
  36. 5. Read English alias pages and show what changes would be made:
  37. python3 scripts/set-alias-page.py -Sn
  38. python3 scripts/set-alias-page.py --sync --dry-run
  39. """
  40. import re
  41. from pathlib import Path
  42. from _common import (
  43. IGNORE_FILES,
  44. Colors,
  45. get_tldr_root,
  46. get_pages_dir,
  47. get_target_paths,
  48. get_locale,
  49. get_status,
  50. stage,
  51. create_colored_line,
  52. create_argument_parser,
  53. )
  54. IGNORE_FILES += ("tldr.md", "aria2.md")
  55. def test_ignore_files():
  56. assert IGNORE_FILES == (
  57. ".DS_Store",
  58. "tldr.md",
  59. "aria2.md",
  60. )
  61. assert ".DS_Store" in IGNORE_FILES
  62. assert "tldr.md" in IGNORE_FILES
  63. def get_templates(root: Path):
  64. """
  65. Get all alias page translation templates from
  66. TLDR_ROOT/contributing-guides/translation-templates/alias-pages.md.
  67. Parameters:
  68. root (Path): The path of local tldr repository, i.e., TLDR_ROOT.
  69. Returns:
  70. dict of (str, str): Language labels map to alias page templates.
  71. """
  72. template_file = root / "contributing-guides/translation-templates/alias-pages.md"
  73. with template_file.open(encoding="utf-8") as f:
  74. lines = f.readlines()
  75. # Parse alias-pages.md
  76. templates = {}
  77. i = 0
  78. while i < len(lines):
  79. if lines[i].startswith("###"):
  80. lang = lines[i][4:].strip("\n").strip(" ")
  81. while True:
  82. i = i + 1
  83. if lines[i].startswith("Not translated yet."):
  84. is_translated = False
  85. break
  86. elif lines[i].startswith("```markdown"):
  87. i = i + 1
  88. is_translated = True
  89. break
  90. if is_translated:
  91. text = ""
  92. while not lines[i].startswith("```"):
  93. text += lines[i]
  94. i = i + 1
  95. templates[lang] = text
  96. i = i + 1
  97. return templates
  98. def set_alias_page(
  99. path: Path, command: str, dry_run: bool = False, language_to_update: str = ""
  100. ) -> str:
  101. """
  102. Write an alias page to disk.
  103. Parameters:
  104. path (string): Path to an alias page
  105. command (string): The command that the alias stands for.
  106. dry_run (bool): Whether to perform a dry-run, i.e. only show the changes that would be made.
  107. language_to_update (string): Optionally, the language of the translation to be updated.
  108. Returns:
  109. str: Execution status
  110. "" if the alias page standing for the same command already exists or if the locale does not match language_to_update.
  111. "\x1b[36mpage added"
  112. "\x1b[34mpage updated"
  113. "\x1b[36mpage would be added"
  114. "\x1b[34mpage would updated"
  115. """
  116. locale = get_locale(path)
  117. if locale not in templates or (
  118. language_to_update != "" and locale != language_to_update
  119. ):
  120. # return empty status to indicate that no changes were made
  121. return ""
  122. alias_name = path.stem
  123. text = (
  124. templates[locale].replace("example", alias_name, 1).replace("example", command)
  125. )
  126. # Test if the alias page already exists
  127. line = re.search(r">.*", text).group(0).replace(command, "(.+)")
  128. original_command = get_alias_page(path, line)
  129. if original_command == command:
  130. return ""
  131. status = get_status(
  132. "added" if original_command == "" else "updated", dry_run, "page"
  133. )
  134. if not dry_run: # Only write to the path during a non-dry-run
  135. path.parent.mkdir(parents=True, exist_ok=True)
  136. with path.open("w", encoding="utf-8") as f:
  137. f.write(text)
  138. return status
  139. def get_alias_page(path: Path, regex: str) -> str:
  140. """
  141. Determine whether the given path is an alias page.
  142. Parameters:
  143. path (Path): Path to a page
  144. Returns:
  145. str: "" If the path doesn't exit or is not an alias page,
  146. otherwise return what command the alias stands for.
  147. """
  148. if not path.exists():
  149. return ""
  150. command_count = 0
  151. command_name = ""
  152. with path.open(encoding="utf-8") as f:
  153. for line in f:
  154. # match alias page pattern "> This command is an alias of `example`."
  155. if match := re.search(regex, line):
  156. command_name = match[1]
  157. # count the lines matching pattern "`...`"
  158. if re.match(r"^`[^`]+`$", line.strip()):
  159. command_count += 1
  160. if command_count == 1:
  161. return command_name
  162. return ""
  163. def sync(
  164. root: Path,
  165. pages_dirs: list[Path],
  166. alias_name: str,
  167. original_command: str,
  168. dry_run: bool = False,
  169. language_to_update: str = "",
  170. ) -> list[Path]:
  171. """
  172. Synchronize an alias page into all translations.
  173. Parameters:
  174. root (Path): TLDR_ROOT
  175. pages_dirs (list of Path's): Path's of page entry and platform, e.g. "page.fr/common".
  176. alias_name (str): An alias command with .md extension like "vi.md".
  177. original_command (str): An Original command like "vim".
  178. dry_run (bool): Whether to perform a dry-run, i.e. only show the changes that would be made.
  179. language_to_update (str): Optionally, the language of the translation to be updated.
  180. Returns:
  181. list (list of Path's): A list of Path's to be staged into git, using by --stage option.
  182. """
  183. paths = []
  184. for page_dir in pages_dirs:
  185. path = root / page_dir / alias_name
  186. status = set_alias_page(path, original_command, dry_run, language_to_update)
  187. if status != "":
  188. rel_path = "/".join(path.parts[-3:])
  189. paths.append(rel_path)
  190. print(create_colored_line(Colors.GREEN, f"{rel_path} {status}"))
  191. return paths
  192. def main():
  193. parser = create_argument_parser(
  194. "Sets the alias page for all translations of a page"
  195. )
  196. parser.add_argument("command", type=str, nargs="?", default="")
  197. args = parser.parse_args()
  198. root = get_tldr_root()
  199. # A dictionary of all alias page translations
  200. global templates
  201. templates = get_templates(root)
  202. pages_dirs = get_pages_dir(root)
  203. target_paths = []
  204. # Use '--page' option
  205. if args.page != "":
  206. target_paths += get_target_paths(args.page, pages_dirs)
  207. for path in target_paths:
  208. rel_path = "/".join(path.parts[-3:])
  209. status = set_alias_page(path, args.command, args.dry_run, args.language)
  210. if status != "":
  211. print(create_colored_line(Colors.GREEN, f"{rel_path} {status}"))
  212. # Use '--sync' option
  213. elif args.sync:
  214. pages_dirs.remove(root / "pages")
  215. en_path = root / "pages"
  216. platforms = [i.name for i in en_path.iterdir() if i.name not in IGNORE_FILES]
  217. for platform in platforms:
  218. platform_path = en_path / platform
  219. commands = [
  220. f"{platform}/{page.name}"
  221. for page in platform_path.iterdir()
  222. if page.name not in IGNORE_FILES
  223. ]
  224. for command in commands:
  225. original_command = get_alias_page(
  226. root / "pages" / command,
  227. r"^> This command is an alias of `(.+)`\.$",
  228. )
  229. if original_command != "":
  230. target_paths += sync(
  231. root,
  232. pages_dirs,
  233. command,
  234. original_command,
  235. args.dry_run,
  236. args.language,
  237. )
  238. # Use '--stage' option
  239. if args.stage and not args.dry_run and len(target_paths) > 0:
  240. stage(target_paths)
  241. if __name__ == "__main__":
  242. main()