completers.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. # Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors.
  2. # Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info.
  3. import argparse
  4. import os
  5. import subprocess
  6. def _call(*args, **kwargs):
  7. # TODO: replace "universal_newlines" with "text" once 3.6 support is dropped
  8. kwargs["universal_newlines"] = True
  9. try:
  10. return subprocess.check_output(*args, **kwargs).splitlines()
  11. except subprocess.CalledProcessError:
  12. return []
  13. class BaseCompleter:
  14. """
  15. This is the base class that all argcomplete completers should subclass.
  16. """
  17. def __call__(
  18. self, *, prefix: str, action: argparse.Action, parser: argparse.ArgumentParser, parsed_args: argparse.Namespace
  19. ) -> None:
  20. raise NotImplementedError("This method should be implemented by a subclass.")
  21. class ChoicesCompleter(BaseCompleter):
  22. def __init__(self, choices):
  23. self.choices = choices
  24. def _convert(self, choice):
  25. if not isinstance(choice, str):
  26. choice = str(choice)
  27. return choice
  28. def __call__(self, **kwargs):
  29. return (self._convert(c) for c in self.choices)
  30. EnvironCompleter = ChoicesCompleter(os.environ)
  31. class FilesCompleter(BaseCompleter):
  32. """
  33. File completer class, optionally takes a list of allowed extensions
  34. """
  35. def __init__(self, allowednames=(), directories=True):
  36. # Fix if someone passes in a string instead of a list
  37. if isinstance(allowednames, (str, bytes)):
  38. allowednames = [allowednames]
  39. self.allowednames = [x.lstrip("*").lstrip(".") for x in allowednames]
  40. self.directories = directories
  41. def __call__(self, prefix, **kwargs):
  42. completion = []
  43. if self.allowednames:
  44. if self.directories:
  45. # Using 'bind' in this and the following commands is a workaround to a bug in bash
  46. # that was fixed in bash 5.3 but affects older versions. Environment variables are not treated
  47. # correctly in older versions and calling bind makes them available. For details, see
  48. # https://savannah.gnu.org/support/index.php?111125
  49. files = _call(
  50. ["bash", "-c", "bind; compgen -A directory -- '{p}'".format(p=prefix)], stderr=subprocess.DEVNULL
  51. )
  52. completion += [f + "/" for f in files]
  53. for x in self.allowednames:
  54. completion += _call(
  55. ["bash", "-c", "bind; compgen -A file -X '!*.{0}' -- '{p}'".format(x, p=prefix)],
  56. stderr=subprocess.DEVNULL,
  57. )
  58. else:
  59. completion += _call(
  60. ["bash", "-c", "bind; compgen -A file -- '{p}'".format(p=prefix)], stderr=subprocess.DEVNULL
  61. )
  62. anticomp = _call(
  63. ["bash", "-c", "bind; compgen -A directory -- '{p}'".format(p=prefix)], stderr=subprocess.DEVNULL
  64. )
  65. completion = list(set(completion) - set(anticomp))
  66. if self.directories:
  67. completion += [f + "/" for f in anticomp]
  68. return completion
  69. class _FilteredFilesCompleter(BaseCompleter):
  70. def __init__(self, predicate):
  71. """
  72. Create the completer
  73. A predicate accepts as its only argument a candidate path and either
  74. accepts it or rejects it.
  75. """
  76. assert predicate, "Expected a callable predicate"
  77. self.predicate = predicate
  78. def __call__(self, prefix, **kwargs):
  79. """
  80. Provide completions on prefix
  81. """
  82. target_dir = os.path.dirname(prefix)
  83. try:
  84. names = os.listdir(target_dir or ".")
  85. except Exception:
  86. return # empty iterator
  87. incomplete_part = os.path.basename(prefix)
  88. # Iterate on target_dir entries and filter on given predicate
  89. for name in names:
  90. if not name.startswith(incomplete_part):
  91. continue
  92. candidate = os.path.join(target_dir, name)
  93. if not self.predicate(candidate):
  94. continue
  95. yield candidate + "/" if os.path.isdir(candidate) else candidate
  96. class DirectoriesCompleter(_FilteredFilesCompleter):
  97. def __init__(self):
  98. _FilteredFilesCompleter.__init__(self, predicate=os.path.isdir)
  99. class SuppressCompleter(BaseCompleter):
  100. """
  101. A completer used to suppress the completion of specific arguments
  102. """
  103. def __init__(self):
  104. pass
  105. def suppress(self):
  106. """
  107. Decide if the completion should be suppressed
  108. """
  109. return True