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