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