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