man_to_zsh.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. #!/usr/bin/env python3
  2. """ Automatically write a zsh completion file. """
  3. import os.path
  4. import re
  5. import shtab
  6. import man_to_argparse
  7. # The zsh generation in shtab can change from version to version, so this
  8. # script is not guaranteed to work with a later (or earlier) shtab.
  9. _HANDLES_SHTAB_VERSION = "1.5.8"
  10. _ZSH_PREAMBLE = r"""# Custom header for the zsh completion file for tarsnap
  11. #
  12. # Place this file in one of the completion folders listed in $fpath (e.g.
  13. # /usr/share/zsh/functions/Completion/Linux or $HOME/.zsh/functions).
  14. # Then either restart zsh, or: autoload -Uz _tarsnap; compdef _tarsnap tarsnap
  15. # Set the following variable to the path of a file containing the output from
  16. # "tarsnap --list-archives", i.e. one archive name per line. If left blank
  17. # then archive names will not be completed.
  18. local archive_list_file=
  19. if [ -n "${archive_list_file}" ]; then
  20. archive_list=( ${(uf)"$(< "${archive_list_file}")"} )
  21. else
  22. archive_list=
  23. fi
  24. """
  25. # -f has a different meaning when it's in --list-archives
  26. SPECIAL_CASE_LIST_ARCHIVES_F = ' "-f[specify hash of archive name' \
  27. ' to operate on (requires --hashes)]:tapehash"'
  28. _AUTO_GENERATED = "# AUTOMATICALLY GENERATED by `shtab`"
  29. _AUTO_GENERATED_US = "%s, then modified by %s" % (_AUTO_GENERATED,
  30. os.path.basename(__file__))
  31. def add_argtypes(zsh_output):
  32. """ Add special zsh completion rules for some arguments. """
  33. outlines = []
  34. for line in zsh_output.split("\n"):
  35. if ':"' in line:
  36. # Special handling for shtab infrastructure in output
  37. if "_shtab_" in line:
  38. outlines.append(line)
  39. continue
  40. # Special handling for "method:arg"
  41. if r"method\:arg" in line:
  42. outlines.append(line)
  43. continue
  44. arg = line.split(":")[-2]
  45. argtype = man_to_argparse.get_argtypestr(arg)
  46. if argtype == "directory":
  47. line = line[:-1] + "{_files -/}\""
  48. elif argtype == "filename":
  49. line = line[:-1] + "{_files}\""
  50. elif argtype == "archive-name":
  51. line = line[:-1] + "(${archive_list}):\""
  52. outlines.append(line)
  53. zsh_output = "\n".join(outlines)
  54. return zsh_output
  55. def restore_metavars(zsh_output, optlist):
  56. """ Replace the argument strings in zsh_output.
  57. shtab uses the option name as the argument string, instead of
  58. metavar (which is what we'd prefer).
  59. """
  60. get_option = re.compile(r"\"(.*)\[")
  61. get_arg = re.compile(r":(.*):\"")
  62. outlines = []
  63. for line in zsh_output.split("\n"):
  64. # Does the line contain an argument?
  65. if ':"' in line:
  66. # Special handling for shtab infrastructure in output
  67. if "_shtab_" in line:
  68. outlines.append(line)
  69. continue
  70. # Find the correct arg to use
  71. opt = get_option.findall(line)[0]
  72. optarg = optlist.get_optarg(opt)
  73. # We need to escape "method:arg", but there's no harm in calling
  74. # this function on every arg in case something else comes up in
  75. # the future.
  76. arg = shtab.escape_zsh(optarg.arg)
  77. # Replace it in the line
  78. line = re.sub(get_arg, ":%s:\"" % arg, line)
  79. outlines.append(line)
  80. zsh_output = "\n".join(outlines)
  81. return zsh_output
  82. def special_case_list_archives_f(zsh_output):
  83. outlines = []
  84. section = ""
  85. for line in zsh_output.split("\n"):
  86. if line.startswith("_shtab_tarsnap__"):
  87. # Get the remainder of that line, other than the last 2 chars
  88. section = line[len("_shtab_tarsnap__"):-2]
  89. elif section != "list_archives_options":
  90. pass
  91. elif not line.startswith(" \"-f[specify name"):
  92. pass
  93. else:
  94. # Modify output of -f for this special case.
  95. line = SPECIAL_CASE_LIST_ARCHIVES_F
  96. outlines.append(line)
  97. zsh_output = "\n".join(outlines)
  98. return zsh_output
  99. def write_zsh(filename_zsh, options, optlist, descs):
  100. """ Write the options into the zsh completion file. """
  101. # Sanity-check shtab version
  102. if shtab.__version__ != _HANDLES_SHTAB_VERSION:
  103. print("ERROR: script designed for shtab %s; found %s instead" % (
  104. _HANDLES_SHTAB_VERSION, shtab.__version__))
  105. exit(1)
  106. # Get a zsh completion file.
  107. parser_obj, _ = man_to_argparse.generate(options, optlist, descs)
  108. zsh_output = shtab.complete(parser_obj, shell="zsh")
  109. # Remind readers that we've modified the completion file.
  110. zsh_output = zsh_output.replace(_AUTO_GENERATED, _AUTO_GENERATED_US)
  111. # Remove the false "mode" strings.
  112. zsh_output = zsh_output.replace("_mode", "").replace("mode", "")
  113. # Use argparse's 'metavar' for argument strings, instead of the 'option'.
  114. zsh_output = restore_metavars(zsh_output, optlist)
  115. # Add special argument completion rules (e.g,. "is a file", "is a dir").
  116. zsh_output = add_argtypes(zsh_output)
  117. # Special-case --list-archives -f TAPEHASH
  118. zsh_output = special_case_list_archives_f(zsh_output)
  119. # Add our custom preamble.
  120. index = zsh_output.find("# AUTO")
  121. zsh_output = "%s%s%s" % (zsh_output[:index], _ZSH_PREAMBLE,
  122. zsh_output[index:])
  123. # Write final completion file
  124. with open(filename_zsh, "wt", encoding="utf-8") as fileobj:
  125. fileobj.write(zsh_output)
  126. fileobj.write("\n")