completion.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import shlex
  2. from argparse import Action
  3. from textwrap import dedent
  4. from borgmatic.commands import arguments
  5. def upgrade_message(language: str, upgrade_command: str, completion_file: str):
  6. return f'''
  7. Your {language} completions script is from a different version of borgmatic than is
  8. currently installed. Please upgrade your script so your completions match the
  9. command-line flags in your installed borgmatic! Try this to upgrade:
  10. {upgrade_command}
  11. source {completion_file}
  12. '''
  13. def parser_flags(parser):
  14. '''
  15. Given an argparse.ArgumentParser instance, return its argument flags in a space-separated
  16. string.
  17. '''
  18. return ' '.join(option for action in parser._actions for option in action.option_strings)
  19. def bash_completion():
  20. '''
  21. Return a bash completion script for the borgmatic command. Produce this by introspecting
  22. borgmatic's command-line argument parsers.
  23. '''
  24. top_level_parser, subparsers = arguments.make_parsers()
  25. global_flags = parser_flags(top_level_parser)
  26. actions = ' '.join(subparsers.choices.keys())
  27. # Avert your eyes.
  28. return '\n'.join(
  29. (
  30. 'check_version() {',
  31. ' local this_script="$(cat "$BASH_SOURCE" 2> /dev/null)"',
  32. ' local installed_script="$(borgmatic --bash-completion 2> /dev/null)"',
  33. ' if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ];'
  34. f''' then cat << EOF\n{upgrade_message(
  35. 'bash',
  36. 'sudo sh -c "borgmatic --bash-completion > $BASH_SOURCE"',
  37. '$BASH_SOURCE',
  38. )}\nEOF''',
  39. ' fi',
  40. '}',
  41. 'complete_borgmatic() {',
  42. )
  43. + tuple(
  44. ''' if [[ " ${COMP_WORDS[*]} " =~ " %s " ]]; then
  45. COMPREPLY=($(compgen -W "%s %s %s" -- "${COMP_WORDS[COMP_CWORD]}"))
  46. return 0
  47. fi'''
  48. % (action, parser_flags(subparser), actions, global_flags)
  49. for action, subparser in subparsers.choices.items()
  50. )
  51. + (
  52. ' COMPREPLY=($(compgen -W "%s %s" -- "${COMP_WORDS[COMP_CWORD]}"))' # noqa: FS003
  53. % (actions, global_flags),
  54. ' (check_version &)',
  55. '}',
  56. '\ncomplete -o bashdefault -o default -F complete_borgmatic borgmatic',
  57. )
  58. )
  59. # fish section
  60. def has_file_options(action: Action):
  61. return action.metavar in (
  62. 'FILENAME',
  63. 'PATH',
  64. ) or action.dest in ('config_paths',)
  65. def has_choice_options(action: Action):
  66. return action.choices is not None
  67. def has_required_param_options(action: Action):
  68. return (
  69. action.nargs
  70. in (
  71. "+",
  72. "*",
  73. )
  74. or '--archive' in action.option_strings
  75. or action.metavar in ('PATTERN', 'KEYS', 'N')
  76. )
  77. def has_exact_options(action: Action):
  78. return (
  79. has_file_options(action) or has_choice_options(action) or has_required_param_options(action)
  80. )
  81. def exact_options_completion(action: Action):
  82. '''
  83. Given an argparse.Action instance, return a completion invocation
  84. that forces file completion or options completion, if the action
  85. takes such an argument and was the last action on the command line.
  86. Otherwise, return an empty string.
  87. '''
  88. if not has_exact_options(action):
  89. return ''
  90. args = ' '.join(action.option_strings)
  91. if has_file_options(action):
  92. return f'''\ncomplete -c borgmatic -Fr -n "__borgmatic_last_arg {args}"'''
  93. if has_choice_options(action):
  94. return f'''\ncomplete -c borgmatic -f -a '{' '.join(map(str, action.choices))}' -n "__borgmatic_last_arg {args}"'''
  95. if has_required_param_options(action):
  96. return f'''\ncomplete -c borgmatic -x -n "__borgmatic_last_arg {args}"'''
  97. raise RuntimeError(
  98. f'Unexpected action: {action} passes has_exact_options but has no choices produced'
  99. )
  100. def dedent_strip_as_tuple(string: str):
  101. return (dedent(string).strip("\n"),)
  102. def fish_completion():
  103. '''
  104. Return a fish completion script for the borgmatic command. Produce this by introspecting
  105. borgmatic's command-line argument parsers.
  106. '''
  107. top_level_parser, subparsers = arguments.make_parsers()
  108. all_subparsers = ' '.join(action for action in subparsers.choices.keys())
  109. exact_option_args = tuple(
  110. ' '.join(action.option_strings)
  111. for subparser in subparsers.choices.values()
  112. for action in subparser._actions
  113. if has_exact_options(action)
  114. ) + tuple(
  115. ' '.join(action.option_strings)
  116. for action in top_level_parser._actions
  117. if len(action.option_strings) > 0
  118. if has_exact_options(action)
  119. )
  120. # Avert your eyes.
  121. return '\n'.join(
  122. dedent_strip_as_tuple(
  123. f'''
  124. function __borgmatic_check_version
  125. set this_filename (status current-filename)
  126. set this_script (cat $this_filename 2> /dev/null)
  127. set installed_script (borgmatic --fish-completion 2> /dev/null)
  128. if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ]
  129. echo "{upgrade_message(
  130. 'fish',
  131. 'borgmatic --fish-completion | sudo tee $this_filename',
  132. '$this_filename',
  133. )}"
  134. end
  135. end
  136. __borgmatic_check_version &
  137. function __borgmatic_last_arg --description 'Check if any of the given arguments are the last on the command line'
  138. set -l all_args (commandline -poc)
  139. # premature optimization to avoid iterating all args if there aren't enough
  140. # to have a last arg beyond borgmatic
  141. if [ (count $all_args) -lt 2 ]
  142. return 1
  143. end
  144. for arg in $argv
  145. if [ "$arg" = "$all_args[-1]" ]
  146. return 0
  147. end
  148. end
  149. return 1
  150. end
  151. set --local subparser_condition "not __fish_seen_subcommand_from {all_subparsers}"
  152. set --local exact_option_condition "not __borgmatic_last_arg {' '.join(exact_option_args)}"
  153. '''
  154. )
  155. + ('\n# subparser completions',)
  156. + tuple(
  157. f'''complete -c borgmatic -f -n "$subparser_condition" -n "$exact_option_condition" -a '{action_name}' -d {shlex.quote(subparser.description)}'''
  158. for action_name, subparser in subparsers.choices.items()
  159. )
  160. + ('\n# global flags',)
  161. + tuple(
  162. f'''complete -c borgmatic -f -n "$exact_option_condition" -a '{' '.join(action.option_strings)}' -d {shlex.quote(action.help)}{exact_options_completion(action)}'''
  163. for action in top_level_parser._actions
  164. if len(action.option_strings) > 0
  165. if 'Deprecated' not in action.help
  166. )
  167. + ('\n# subparser flags',)
  168. + tuple(
  169. f'''complete -c borgmatic -f -n "$exact_option_condition" -a '{' '.join(action.option_strings)}' -d {shlex.quote(action.help)} -n "__fish_seen_subcommand_from {action_name}"{exact_options_completion(action)}'''
  170. for action_name, subparser in subparsers.choices.items()
  171. for action in subparser._actions
  172. if 'Deprecated' not in action.help
  173. )
  174. )