Coverage for typer/_completion_shared.py: 100%
113 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-19 20:29 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-19 20:29 +0000
1import os 1defghiabc
2import re 1defghiabc
3import subprocess 1defghiabc
4from enum import Enum 1defghiabc
5from pathlib import Path 1defghiabc
6from typing import Optional, Tuple, Union 1defghiabc
8import click 1defghiabc
9from typer.core import HAS_SHELLINGHAM 1defghiabc
11if HAS_SHELLINGHAM: 1defghiabc
12 import shellingham 1defghiabc
15class Shells(str, Enum): 1defghiabc
16 bash = "bash" 1defghiabc
17 zsh = "zsh" 1defghiabc
18 fish = "fish" 1defghiabc
19 powershell = "powershell" 1defghiabc
20 pwsh = "pwsh" 1defghiabc
23COMPLETION_SCRIPT_BASH = """ 1defghiabc
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 = """ 1defghiabc
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"' 1defghiabc
47COMPLETION_SCRIPT_POWER_SHELL = """ 1defghiabc
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 = { 1defghiabc
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_]") 1defghiabc
81def get_completion_script(*, prog_name: str, complete_var: str, shell: str) -> str: 1defghiabc
82 cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_")) 1defghiabc
83 script = _completion_scripts.get(shell) 1defghiabc
84 if script is None: 1defghiabc
85 click.echo(f"Shell {shell} not supported.", err=True) 1defghiabc
86 raise click.exceptions.Exit(1) 1defghiabc
87 return ( 1defghiabc
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: 1defghiabc
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" 1defghiabc
103 rc_path = Path.home() / ".bashrc" 1defghiabc
104 rc_path.parent.mkdir(parents=True, exist_ok=True) 1defghiabc
105 rc_content = "" 1defghiabc
106 if rc_path.is_file(): 1defghiabc
107 rc_content = rc_path.read_text() 1defabc
108 completion_init_lines = [f"source '{completion_path}'"] 1defghiabc
109 for line in completion_init_lines: 1defghiabc
110 if line not in rc_content: # pragma: no cover 1defghiabc
111 rc_content += f"\n{line}" 1defghiabc
112 rc_content += "\n" 1defghiabc
113 rc_path.write_text(rc_content) 1defghiabc
114 # Install completion
115 completion_path.parent.mkdir(parents=True, exist_ok=True) 1defghiabc
116 script_content = get_completion_script( 1defghiabc
117 prog_name=prog_name, complete_var=complete_var, shell=shell
118 )
119 completion_path.write_text(script_content) 1defghiabc
120 return completion_path 1defghiabc
123def install_zsh(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defghiabc
124 # Setup Zsh and load ~/.zfunc
125 zshrc_path = Path.home() / ".zshrc" 1defghiabc
126 zshrc_path.parent.mkdir(parents=True, exist_ok=True) 1defghiabc
127 zshrc_content = "" 1defghiabc
128 if zshrc_path.is_file(): 1defghiabc
129 zshrc_content = zshrc_path.read_text() 1defghiabc
130 completion_line = "fpath+=~/.zfunc; autoload -Uz compinit; compinit" 1defghiabc
131 if completion_line not in zshrc_content: 1defghiabc
132 zshrc_content += f"\n{completion_line}\n" 1defghiabc
133 style_line = "zstyle ':completion:*' menu select" 1defghiabc
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: 1defghiabc
138 zshrc_content += f"\n{style_line}\n" 1defghiabc
139 zshrc_content = f"{zshrc_content.strip()}\n" 1defghiabc
140 zshrc_path.write_text(zshrc_content) 1defghiabc
141 # Install completion under ~/.zfunc/
142 path_obj = Path.home() / f".zfunc/_{prog_name}" 1defghiabc
143 path_obj.parent.mkdir(parents=True, exist_ok=True) 1defghiabc
144 script_content = get_completion_script( 1defghiabc
145 prog_name=prog_name, complete_var=complete_var, shell=shell
146 )
147 path_obj.write_text(script_content) 1defghiabc
148 return path_obj 1defghiabc
151def install_fish(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defghiabc
152 path_obj = Path.home() / f".config/fish/completions/{prog_name}.fish" 1defghiabc
153 parent_dir: Path = path_obj.parent 1defghiabc
154 parent_dir.mkdir(parents=True, exist_ok=True) 1defghiabc
155 script_content = get_completion_script( 1defghiabc
156 prog_name=prog_name, complete_var=complete_var, shell=shell
157 )
158 path_obj.write_text(f"{script_content}\n") 1defghiabc
159 return path_obj 1defghiabc
162def install_powershell(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defghiabc
163 subprocess.run( 1defghiabc
164 [
165 shell,
166 "-Command",
167 "Set-ExecutionPolicy",
168 "Unrestricted",
169 "-Scope",
170 "CurrentUser",
171 ]
172 )
173 result = subprocess.run( 1defghiabc
174 [shell, "-NoProfile", "-Command", "echo", "$profile"],
175 check=True,
176 stdout=subprocess.PIPE,
177 )
178 if result.returncode != 0: # pragma: no cover 1defghiabc
179 click.echo("Couldn't get PowerShell user profile", err=True)
180 raise click.exceptions.Exit(result.returncode)
181 path_str = "" 1defghiabc
182 if isinstance(result.stdout, str): # pragma: no cover 1defghiabc
183 path_str = result.stdout
184 if isinstance(result.stdout, bytes): 1defghiabc
185 for encoding in ["windows-1252", "utf8", "cp850"]: 1defghiabc
186 try: 1defghiabc
187 path_str = result.stdout.decode(encoding) 1defghiabc
188 break 1defghiabc
189 except UnicodeDecodeError: # pragma: no cover
190 pass
191 if not path_str: # pragma: no cover 1defghiabc
192 click.echo("Couldn't decode the path automatically", err=True)
193 raise click.exceptions.Exit(1)
194 path_obj = Path(path_str.strip()) 1defghiabc
195 parent_dir: Path = path_obj.parent 1defghiabc
196 parent_dir.mkdir(parents=True, exist_ok=True) 1defghiabc
197 script_content = get_completion_script( 1defghiabc
198 prog_name=prog_name, complete_var=complete_var, shell=shell
199 )
200 with path_obj.open(mode="a") as f: 1defghiabc
201 f.write(f"{script_content}\n") 1defghiabc
202 return path_obj 1defghiabc
205def install( 1defghiabc
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 1defghiabc
211 assert prog_name 1defghiabc
212 if complete_var is None: 1defghiabc
213 complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper()) 1defghiabc
214 test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION") 1defghiabc
215 if shell is None and not test_disable_detection: 1defghiabc
216 shell = _get_shell_name() 1defghiabc
217 if shell == "bash": 1defghiabc
218 installed_path = install_bash( 1defghiabc
219 prog_name=prog_name, complete_var=complete_var, shell=shell
220 )
221 return shell, installed_path 1defghiabc
222 elif shell == "zsh": 1defghiabc
223 installed_path = install_zsh( 1defghiabc
224 prog_name=prog_name, complete_var=complete_var, shell=shell
225 )
226 return shell, installed_path 1defghiabc
227 elif shell == "fish": 1defghiabc
228 installed_path = install_fish( 1defghiabc
229 prog_name=prog_name, complete_var=complete_var, shell=shell
230 )
231 return shell, installed_path 1defghiabc
232 elif shell in {"powershell", "pwsh"}: 1defghiabc
233 installed_path = install_powershell( 1defghiabc
234 prog_name=prog_name, complete_var=complete_var, shell=shell
235 )
236 return shell, installed_path 1defghiabc
237 else:
238 click.echo(f"Shell {shell} is not supported.") 1defghiabc
239 raise click.exceptions.Exit(1) 1defghiabc
242def _get_shell_name() -> Union[str, None]: 1defghiabc
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: 1defghiabc
250 try: 1defghiabc
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 1defghiabc
254 except shellingham.ShellDetectionFailure: # pragma: no cover 1abc
255 name = None 1abc
256 else:
257 name = None # pragma: no cover
259 return name 1defghiabc