set-alias-page.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  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. Note: This script uses an interactive prompt instead of positional arguments to:
  9. - Prevent argument parsing errors with command names containing dashes (e.g. 'pacman -S')
  10. - Provide clearer guidance for required inputs
  11. - Allow for input validation before page creation
  12. Usage:
  13. python3 scripts/set-alias-page.py [-p PAGE] [-S] [-l LANGUAGE] [-s] [-n]
  14. Options:
  15. -p, --page PAGE
  16. Specify the alias page in the format "platform/alias_command.md".
  17. This will start an interactive prompt to create/update the page.
  18. -S, --sync
  19. Synchronize each translation's alias page (if exists) with that of the English page.
  20. -l, --language LANGUAGE
  21. Specify the language, a POSIX Locale Name in the form of "ll" or "ll_CC" (e.g. "fr" or "pt_BR").
  22. -s, --stage
  23. Stage modified pages (requires 'git' on $PATH and TLDR_ROOT to be a Git repository).
  24. -n, --dry-run
  25. Show what changes would be made without actually modifying the page.
  26. Examples:
  27. 1. Create a new alias page interactively:
  28. python3 scripts/set-alias-page.py -p osx/gsum
  29. python3 scripts/set-alias-page.py --page osx/gsum
  30. This will start a wizard that guides you through creating the page.
  31. 2. Read English alias pages and synchronize them into all translations:
  32. python3 scripts/set-alias-page.py -S
  33. python3 scripts/set-alias-page.py --sync
  34. 3. Read English alias pages and synchronize them for Brazilian Portuguese pages only:
  35. python3 scripts/set-alias-page.py -S -l pt_BR
  36. python3 scripts/set-alias-page.py --sync --language pt_BR
  37. 4. Read English alias pages, synchronize them into all translations and stage modified pages for commit:
  38. python3 scripts/set-alias-page.py -Ss
  39. python3 scripts/set-alias-page.py --sync --stage
  40. 5. Read English alias pages and show what changes would be made:
  41. python3 scripts/set-alias-page.py -Sn
  42. python3 scripts/set-alias-page.py --sync --dry-run
  43. """
  44. import re
  45. from pathlib import Path
  46. from dataclasses import dataclass
  47. from _common import (
  48. IGNORE_FILES,
  49. Colors,
  50. get_tldr_root,
  51. get_pages_dir,
  52. get_target_paths,
  53. get_locale,
  54. get_status,
  55. stage,
  56. create_colored_line,
  57. create_argument_parser,
  58. )
  59. @dataclass
  60. class Config:
  61. """Global configuration for the script"""
  62. root: Path
  63. pages_dirs: list[Path]
  64. templates: dict[str, str]
  65. dry_run: bool = False
  66. language: str = ""
  67. @dataclass
  68. class AliasPageContent:
  69. """Content of an alias page"""
  70. title: str
  71. original_command: str
  72. documentation_command: str
  73. @dataclass
  74. class AliasPage:
  75. """Represents an alias page with its path and content"""
  76. page_path: str
  77. content: AliasPageContent
  78. IGNORE_FILES += ("tldr.md", "aria2.md")
  79. def test_ignore_files():
  80. assert IGNORE_FILES == (
  81. ".DS_Store",
  82. "tldr.md",
  83. "aria2.md",
  84. )
  85. assert ".DS_Store" in IGNORE_FILES
  86. assert "tldr.md" in IGNORE_FILES
  87. def get_templates(root: Path):
  88. """
  89. Get all alias page translation templates from
  90. TLDR_ROOT/contributing-guides/translation-templates/alias-pages.md.
  91. Parameters:
  92. root (Path): The path of local tldr repository, i.e., TLDR_ROOT.
  93. Returns:
  94. dict of (str, str): Language labels map to alias page templates.
  95. """
  96. template_file = root / "contributing-guides/translation-templates/alias-pages.md"
  97. with template_file.open(encoding="utf-8") as f:
  98. lines = f.readlines()
  99. # Parse alias-pages.md
  100. templates = {}
  101. i = 0
  102. while i < len(lines):
  103. if lines[i].startswith("###"):
  104. lang = lines[i][4:].strip("\n").strip(" ")
  105. while True:
  106. i = i + 1
  107. if lines[i].startswith("Not translated yet."):
  108. is_translated = False
  109. break
  110. elif lines[i].startswith("```markdown"):
  111. i = i + 1
  112. is_translated = True
  113. break
  114. if is_translated:
  115. text = ""
  116. while not lines[i].startswith("```"):
  117. text += lines[i]
  118. i = i + 1
  119. templates[lang] = text
  120. i = i + 1
  121. return templates
  122. def generate_alias_page_content(
  123. template_content: str,
  124. page_content: AliasPageContent,
  125. ) -> str:
  126. """
  127. Generate alias page content by replacing placeholders in the template.
  128. Parameters:
  129. template_content (str): The markdown template for the specific language.
  130. page_content (AliasPageContent): The content of the alias page
  131. Returns:
  132. str: The complete markdown content for the alias page.
  133. """
  134. template_command = "example"
  135. # Replace placeholders in template with actual values
  136. result = template_content.replace(template_command, page_content.title, 1)
  137. result = result.replace(template_command, page_content.original_command, 1)
  138. result = result.replace(template_command, page_content.documentation_command)
  139. return result
  140. def set_alias_page(
  141. path: Path,
  142. page_content: AliasPageContent,
  143. ) -> str:
  144. """
  145. Write an alias page to disk.
  146. Parameters:
  147. path (Path): Path to an alias page
  148. page_content (AliasPageContent): The content to write to the page
  149. Returns:
  150. str: Execution status
  151. "" if the alias page standing for the same command already exists or if the locale does not match language_to_update.
  152. "\x1b[36mpage added"
  153. "\x1b[34mpage updated"
  154. "\x1b[36mpage would be added"
  155. "\x1b[34mpage would updated"
  156. """
  157. locale = get_locale(path)
  158. if locale not in config.templates or (
  159. config.language != "" and locale != config.language
  160. ):
  161. return ""
  162. # Get existing alias command from the locale page
  163. existing_locale_page_content = get_alias_command_in_page(
  164. path, get_locale_alias_pattern(locale)
  165. )
  166. if (
  167. existing_locale_page_content.documentation_command
  168. == page_content.documentation_command
  169. ):
  170. return ""
  171. new_locale_page_content = generate_alias_page_content(
  172. config.templates[locale],
  173. page_content,
  174. )
  175. # Determine status and write file
  176. status = get_status(
  177. "added" if not path.exists() else "updated",
  178. config.dry_run,
  179. "page",
  180. )
  181. if not config.dry_run:
  182. path.parent.mkdir(parents=True, exist_ok=True)
  183. with path.open("w", encoding="utf-8") as f:
  184. f.write(new_locale_page_content)
  185. return status
  186. def get_locale_alias_pattern(locale: str) -> str:
  187. """Get alias pattern from template"""
  188. template_line = re.search(r">.*`example`", config.templates[locale]).group(0)
  189. locale_alias_pattern = template_line[2 : template_line.find("`example`")].strip()
  190. return locale_alias_pattern
  191. def get_alias_command_in_page(path: Path, alias_pattern: str) -> AliasPageContent:
  192. """
  193. Determine whether the given path is an alias page.
  194. Returns:
  195. AliasPageContent: The page content, or empty strings if not an alias page
  196. """
  197. if not path.exists():
  198. return AliasPageContent(title="", original_command="", documentation_command="")
  199. with path.open(encoding="utf-8") as f:
  200. content = f.read()
  201. lines = content.splitlines()
  202. title = next((line.strip("# \n") for line in lines if line.startswith("# ")), "")
  203. command_lines = [line for line in lines if "`" in line]
  204. if len(command_lines) != 2 or not title:
  205. return AliasPageContent(title="", original_command="", documentation_command="")
  206. original_command = ""
  207. documentation_command = ""
  208. alias_line = next((line for line in command_lines if alias_pattern in line), None)
  209. if alias_line:
  210. description_match = re.search(r"`([^`]+)`", alias_line)
  211. if description_match:
  212. original_command = description_match[1]
  213. tldr_line = next(
  214. (line for line in command_lines if line.strip().startswith("`tldr")), None
  215. )
  216. if tldr_line:
  217. tldr_match = re.search(r"`tldr (.+)`", tldr_line.strip())
  218. if tldr_match:
  219. documentation_command = tldr_match[1]
  220. return AliasPageContent(
  221. title=title,
  222. original_command=original_command,
  223. documentation_command=documentation_command,
  224. )
  225. def sync_alias_page_to_locale(pages_dir: Path, alias_page: AliasPage) -> list[Path]:
  226. """
  227. Synchronize an alias page into a specific locale directory.
  228. Parameters:
  229. pages_dir (Path): Directory containing pages for a specific locale
  230. alias_page (AliasPage): The alias page to sync
  231. Returns:
  232. list[Path]: List of paths that were modified
  233. """
  234. paths = []
  235. path = config.root / pages_dir / alias_page.page_path
  236. status = set_alias_page(path, alias_page.content)
  237. if status != "":
  238. rel_path = "/".join(path.parts[-3:])
  239. paths.append(rel_path)
  240. print(create_colored_line(Colors.GREEN, f"{rel_path} {status}"))
  241. return paths
  242. def get_english_alias_pages(en_path: Path) -> list[AliasPage]:
  243. """
  244. Get all English alias pages with their commands.
  245. Parameters:
  246. en_path (Path): Path to English pages directory
  247. Returns:
  248. list[AliasPage]: List of alias pages with their content
  249. """
  250. alias_pages = []
  251. alias_pattern = get_locale_alias_pattern("en")
  252. # Get all platform directories (common, linux, etc.)
  253. platforms = [
  254. page.name for page in en_path.iterdir() if page.name not in IGNORE_FILES
  255. ]
  256. # Iterate through each platform
  257. for platform in platforms:
  258. platform_path = en_path / platform
  259. page_paths = [
  260. f"{platform}/{page.name}"
  261. for page in platform_path.iterdir()
  262. if page.name not in IGNORE_FILES
  263. ]
  264. # Check each command if it's an alias
  265. for page_path in page_paths:
  266. page_content = get_alias_command_in_page(en_path / page_path, alias_pattern)
  267. if page_content.original_command:
  268. alias_pages.append(AliasPage(page_path=page_path, content=page_content))
  269. return alias_pages
  270. def prompt_alias_page_info(page_path: str) -> AliasPageContent:
  271. """
  272. Prompt user for alias page content.
  273. Returns:
  274. AliasPageContent: The collected page content
  275. """
  276. en_path = config.root / "pages"
  277. if not page_path.lower().endswith(".md"):
  278. page_path = f"{page_path}.md"
  279. exists = (en_path / page_path).exists()
  280. print(f"\n{'Updating' if exists else 'Creating new'} alias page...")
  281. print(create_colored_line(Colors.CYAN, f"Page path: {page_path}"))
  282. print(
  283. create_colored_line(
  284. Colors.BLUE,
  285. "\nThe title will be used in the first line of the page after '#'",
  286. )
  287. )
  288. print(create_colored_line(Colors.GREEN, "Example: npm run-script"))
  289. title = input(create_colored_line(Colors.CYAN, "Enter page title: ")).strip()
  290. if not title:
  291. raise SystemExit(create_colored_line(Colors.RED, "Title cannot be empty"))
  292. print(
  293. create_colored_line(
  294. Colors.BLUE,
  295. "\nThe original command will appear in 'This command is an alias of `command`'",
  296. )
  297. )
  298. print(create_colored_line(Colors.GREEN, "Example: npm run"))
  299. original_command = input(
  300. create_colored_line(Colors.CYAN, "Enter original command: ")
  301. ).strip()
  302. if not original_command:
  303. raise SystemExit(
  304. create_colored_line(Colors.RED, "Original command cannot be empty")
  305. )
  306. print(
  307. create_colored_line(
  308. Colors.BLUE,
  309. "\nThe documentation command will be used in 'tldr command' line",
  310. )
  311. )
  312. print(create_colored_line(Colors.GREEN, "Example: npm run"))
  313. documentation_command = input(
  314. create_colored_line(
  315. Colors.CYAN,
  316. f"Enter documentation command (press Enter to use {original_command}): ",
  317. )
  318. ).strip()
  319. if not documentation_command:
  320. documentation_command = original_command
  321. print("\nSummary:")
  322. print(f"* Title: {create_colored_line(Colors.CYAN, title)}")
  323. print(f"* Original command: {create_colored_line(Colors.CYAN, original_command)}")
  324. print(
  325. f"* Documentation command: {create_colored_line(Colors.CYAN, documentation_command)}"
  326. )
  327. print(create_colored_line(Colors.BLUE, "\nThis will create a page like:"))
  328. print(create_colored_line(Colors.GREEN, f"# {title}"))
  329. print(
  330. create_colored_line(
  331. Colors.GREEN, f"\n> This command is an alias of `{original_command}`."
  332. )
  333. )
  334. print(
  335. create_colored_line(
  336. Colors.GREEN, "\n- View documentation for the original command:"
  337. )
  338. )
  339. print(create_colored_line(Colors.GREEN, f"\n`tldr {documentation_command}`"))
  340. response = (
  341. input(create_colored_line(Colors.CYAN, "\nProceed? [Y/n] ")).lower().strip()
  342. )
  343. if response and response not in ["y", "yes"]:
  344. raise SystemExit(create_colored_line(Colors.RED, "Cancelled by user"))
  345. return AliasPageContent(
  346. title=title,
  347. original_command=original_command,
  348. documentation_command=documentation_command,
  349. )
  350. def main():
  351. parser = create_argument_parser(
  352. "Sets the alias page for all translations of a page"
  353. )
  354. args = parser.parse_args()
  355. root = get_tldr_root()
  356. pages_dirs = get_pages_dir(root)
  357. templates = get_templates(root)
  358. global config
  359. config = Config(
  360. root=root,
  361. pages_dirs=pages_dirs,
  362. templates=templates,
  363. dry_run=args.dry_run,
  364. language=args.language,
  365. )
  366. target_paths = []
  367. # Use '--page' option
  368. if args.page != "":
  369. page_info = prompt_alias_page_info(args.page)
  370. target_paths += get_target_paths(
  371. args.page, config.pages_dirs, check_exists=False
  372. )
  373. for path in target_paths:
  374. rel_path = "/".join(path.parts[-3:])
  375. status = set_alias_page(path, page_info)
  376. if status != "":
  377. print(create_colored_line(Colors.GREEN, f"{rel_path} {status}"))
  378. # Use '--sync' option
  379. elif args.sync:
  380. en_path = config.root / "pages"
  381. pages_dirs = config.pages_dirs.copy()
  382. pages_dirs.remove(en_path)
  383. alias_pages = get_english_alias_pages(en_path)
  384. for alias_page in alias_pages:
  385. for pages_dir in pages_dirs:
  386. target_paths.extend(sync_alias_page_to_locale(pages_dir, alias_page))
  387. # Use '--stage' option
  388. if args.stage and not config.dry_run and len(target_paths) > 0:
  389. stage(target_paths)
  390. if __name__ == "__main__":
  391. main()