Coverage for typer / _completion_shared.py: 100%
111 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
1import os 1defgabc
2import re 1defgabc
3import subprocess 1defgabc
4from enum import Enum 1defgabc
5from pathlib import Path 1defgabc
7import shellingham 1defgabc
9from . import _click 1defgabc
10from ._click.globals import get_current_context 1defgabc
11from .exceptions import Exit 1defgabc
14class Shells(str, Enum): 1defgabc
15 bash = "bash" 1defgabc
16 zsh = "zsh" 1defgabc
17 fish = "fish" 1defgabc
18 powershell = "powershell" 1defgabc
19 pwsh = "pwsh" 1defgabc
22COMPLETION_SCRIPT_BASH = """ 1defgabc
23%(complete_func)s() {
24 local IFS=$'\n'
25 COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
26 COMP_CWORD=$COMP_CWORD \\
27 %(autocomplete_var)s=complete_bash $1 ) )
28 return 0
29}
31complete -o default -F %(complete_func)s %(prog_name)s
32"""
34COMPLETION_SCRIPT_ZSH = """ 1defgabc
35#compdef %(prog_name)s
37%(complete_func)s() {
38 eval $(env _TYPER_COMPLETE_ARGS="${words[1,$CURRENT]}" %(autocomplete_var)s=complete_zsh %(prog_name)s)
39}
41compdef %(complete_func)s %(prog_name)s
42"""
44COMPLETION_SCRIPT_FISH = 'complete --command %(prog_name)s --no-files --arguments "(env %(autocomplete_var)s=complete_fish _TYPER_COMPLETE_FISH_ACTION=get-args _TYPER_COMPLETE_ARGS=(commandline -cp) %(prog_name)s)" --condition "env %(autocomplete_var)s=complete_fish _TYPER_COMPLETE_FISH_ACTION=is-args _TYPER_COMPLETE_ARGS=(commandline -cp) %(prog_name)s"' 1defgabc
46COMPLETION_SCRIPT_POWER_SHELL = """ 1defgabc
47Import-Module PSReadLine
48Set-PSReadLineKeyHandler -Chord Tab -Function MenuComplete
49$scriptblock = {
50 param($wordToComplete, $commandAst, $cursorPosition)
51 $Env:%(autocomplete_var)s = "complete_powershell"
52 $Env:_TYPER_COMPLETE_ARGS = $commandAst.ToString()
53 $Env:_TYPER_COMPLETE_WORD_TO_COMPLETE = $wordToComplete
54 %(prog_name)s | ForEach-Object {
55 $commandArray = $_ -Split ":::"
56 $command = $commandArray[0]
57 $helpString = $commandArray[1]
58 [System.Management.Automation.CompletionResult]::new(
59 $command, $command, 'ParameterValue', $helpString)
60 }
61 $Env:%(autocomplete_var)s = ""
62 $Env:_TYPER_COMPLETE_ARGS = ""
63 $Env:_TYPER_COMPLETE_WORD_TO_COMPLETE = ""
64}
65Register-ArgumentCompleter -Native -CommandName %(prog_name)s -ScriptBlock $scriptblock
66"""
68_completion_scripts = { 1defgabc
69 "bash": COMPLETION_SCRIPT_BASH,
70 "zsh": COMPLETION_SCRIPT_ZSH,
71 "fish": COMPLETION_SCRIPT_FISH,
72 "powershell": COMPLETION_SCRIPT_POWER_SHELL,
73 "pwsh": COMPLETION_SCRIPT_POWER_SHELL,
74}
76# TODO: Probably refactor this, copied from Click 7.x
77_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]") 1defgabc
80def get_completion_script(*, prog_name: str, complete_var: str, shell: str) -> str: 1defgabc
81 cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_")) 1defgabc
82 script = _completion_scripts.get(shell) 1defgabc
83 if script is None: 1defgabc
84 _click.echo(f"Shell {shell} not supported.", err=True) 1defgabc
85 raise Exit(1) 1defgabc
86 return ( 1defgabc
87 script
88 % {
89 "complete_func": f"_{cf_name}_completion",
90 "prog_name": prog_name,
91 "autocomplete_var": complete_var,
92 }
93 ).strip()
96def install_bash(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defgabc
97 # Ref: https://github.com/scop/bash-completion#faq
98 # It seems bash-completion is the official completion system for bash:
99 # Ref: https://www.gnu.org/software/bash/manual/html_node/A-Programmable-Completion-Example.html
100 # But installing in the locations from the docs doesn't seem to have effect
101 completion_path = Path.home() / ".bash_completions" / f"{prog_name}.sh" 1defgabc
102 rc_path = Path.home() / ".bashrc" 1defgabc
103 rc_path.parent.mkdir(parents=True, exist_ok=True) 1defgabc
104 rc_content = "" 1defgabc
105 if rc_path.is_file(): 1defgabc
106 rc_content = rc_path.read_text() 1deabc
107 completion_init_lines = [f"source '{completion_path}'"] 1defgabc
108 for line in completion_init_lines: 1defgabc
109 if line not in rc_content: # pragma: no cover 1defgabc
110 rc_content += f"\n{line}" 1defgabc
111 rc_content += "\n" 1defgabc
112 rc_path.write_text(rc_content) 1defgabc
113 # Install completion
114 completion_path.parent.mkdir(parents=True, exist_ok=True) 1defgabc
115 script_content = get_completion_script( 1defgabc
116 prog_name=prog_name, complete_var=complete_var, shell=shell
117 )
118 completion_path.write_text(script_content) 1defgabc
119 return completion_path 1defgabc
122def install_zsh(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defgabc
123 # Setup Zsh and load ~/.zfunc
124 zshrc_path = Path.home() / ".zshrc" 1defgabc
125 zshrc_path.parent.mkdir(parents=True, exist_ok=True) 1defgabc
126 zshrc_content = "" 1defgabc
127 if zshrc_path.is_file(): 1defgabc
128 zshrc_content = zshrc_path.read_text() 1defgabc
129 completion_line = "fpath+=~/.zfunc; autoload -Uz compinit; compinit" 1defgabc
130 if completion_line not in zshrc_content: 1defgabc
131 zshrc_content += f"\n{completion_line}\n" 1defgabc
132 style_line = "zstyle ':completion:*' menu select" 1defgabc
133 # TODO: consider setting the style only for the current program
134 # style_line = f"zstyle ':completion:*:*:{prog_name}:*' menu select"
135 # Install zstyle completion config only if the user doesn't have a customization
136 if "zstyle" not in zshrc_content: 1defgabc
137 zshrc_content += f"\n{style_line}\n" 1defgabc
138 zshrc_content = f"{zshrc_content.strip()}\n" 1defgabc
139 zshrc_path.write_text(zshrc_content) 1defgabc
140 # Install completion under ~/.zfunc/
141 path_obj = Path.home() / f".zfunc/_{prog_name}" 1defgabc
142 path_obj.parent.mkdir(parents=True, exist_ok=True) 1defgabc
143 script_content = get_completion_script( 1defgabc
144 prog_name=prog_name, complete_var=complete_var, shell=shell
145 )
146 path_obj.write_text(script_content) 1defgabc
147 return path_obj 1defgabc
150def install_fish(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defgabc
151 path_obj = Path.home() / f".config/fish/completions/{prog_name}.fish" 1defgabc
152 parent_dir: Path = path_obj.parent 1defgabc
153 parent_dir.mkdir(parents=True, exist_ok=True) 1defgabc
154 script_content = get_completion_script( 1defgabc
155 prog_name=prog_name, complete_var=complete_var, shell=shell
156 )
157 path_obj.write_text(f"{script_content}\n") 1defgabc
158 return path_obj 1defgabc
161def install_powershell(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defgabc
162 subprocess.run( 1defgabc
163 [
164 shell,
165 "-Command",
166 "Set-ExecutionPolicy",
167 "Unrestricted",
168 "-Scope",
169 "CurrentUser",
170 ]
171 )
172 result = subprocess.run( 1defgabc
173 [shell, "-NoProfile", "-Command", "echo", "$profile"],
174 check=True,
175 stdout=subprocess.PIPE,
176 )
177 if result.returncode != 0: # pragma: no cover 1defgabc
178 _click.echo("Couldn't get PowerShell user profile", err=True)
179 raise Exit(result.returncode)
180 path_str = "" 1defgabc
181 if isinstance(result.stdout, str): # pragma: no cover 1defgabc
182 path_str = result.stdout
183 if isinstance(result.stdout, bytes): 1defgabc
184 for encoding in ["windows-1252", "utf8", "cp850"]: 1defgabc
185 try: 1defgabc
186 path_str = result.stdout.decode(encoding) 1defgabc
187 break 1defgabc
188 except UnicodeDecodeError: # pragma: no cover
189 pass
190 if not path_str: # pragma: no cover 1defgabc
191 _click.echo("Couldn't decode the path automatically", err=True)
192 raise Exit(1)
193 path_obj = Path(path_str.strip()) 1defgabc
194 parent_dir: Path = path_obj.parent 1defgabc
195 parent_dir.mkdir(parents=True, exist_ok=True) 1defgabc
196 script_content = get_completion_script( 1defgabc
197 prog_name=prog_name, complete_var=complete_var, shell=shell
198 )
199 with path_obj.open(mode="a") as f: 1defgabc
200 f.write(f"{script_content}\n") 1defgabc
201 return path_obj 1defgabc
204def install( 1defgabc
205 shell: str | None = None,
206 prog_name: str | None = None,
207 complete_var: str | None = None,
208) -> tuple[str, Path]:
209 prog_name = prog_name or get_current_context().find_root().info_name 1defgabc
210 assert prog_name 1defgabc
211 if complete_var is None: 1defgabc
212 complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper()) 1defgabc
213 test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION") 1defgabc
214 if shell is None and not test_disable_detection: 1defgabc
215 shell = _get_shell_name() 1defgabc
216 if shell == "bash": 1defgabc
217 installed_path = install_bash( 1defgabc
218 prog_name=prog_name, complete_var=complete_var, shell=shell
219 )
220 return shell, installed_path 1defgabc
221 elif shell == "zsh": 1defgabc
222 installed_path = install_zsh( 1defgabc
223 prog_name=prog_name, complete_var=complete_var, shell=shell
224 )
225 return shell, installed_path 1defgabc
226 elif shell == "fish": 1defgabc
227 installed_path = install_fish( 1defgabc
228 prog_name=prog_name, complete_var=complete_var, shell=shell
229 )
230 return shell, installed_path 1defgabc
231 elif shell in {"powershell", "pwsh"}: 1defgabc
232 installed_path = install_powershell( 1defgabc
233 prog_name=prog_name, complete_var=complete_var, shell=shell
234 )
235 return shell, installed_path 1defgabc
236 else:
237 _click.echo(f"Shell {shell} is not supported.") 1defgabc
238 raise Exit(1) 1defgabc
241def _get_shell_name() -> str | None: 1defgabc
242 """Get the current shell name, if available.
244 The name will always be lowercase. If the shell cannot be detected, None is
245 returned.
246 """
247 name: str | None # N.B. shellingham is untyped
248 try: 1defgabc
249 # N.B. detect_shell returns a tuple of (shell name, shell command).
250 # We only need the name.
251 name, _cmd = shellingham.detect_shell() # noqa: TID251 1defgabc
252 except shellingham.ShellDetectionFailure: # pragma: no cover 1abc
253 name = None 1abc
255 return name 1defgabc