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