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