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

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

7 

8import click 1cdefghab

9from typer.core import HAS_SHELLINGHAM 1cdefghab

10 

11if HAS_SHELLINGHAM: 1cdefghab

12 import shellingham 1cdefghab

13 

14 

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

21 

22 

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} 

31 

32complete -o default -F %(complete_func)s %(prog_name)s 

33""" 

34 

35COMPLETION_SCRIPT_ZSH = """ 1cdefghab

36#compdef %(prog_name)s 

37 

38%(complete_func)s() { 

39 eval $(env _TYPER_COMPLETE_ARGS="${words[1,$CURRENT]}" %(autocomplete_var)s=complete_zsh %(prog_name)s) 

40} 

41 

42compdef %(complete_func)s %(prog_name)s 

43""" 

44 

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

46 

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""" 

68 

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} 

76 

77# TODO: Probably refactor this, copied from Click 7.x 

78_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]") 1cdefghab

79 

80 

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() 

95 

96 

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

121 

122 

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

149 

150 

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

160 

161 

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

203 

204 

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

240 

241 

242def _get_shell_name() -> Union[str, None]: 1cdefghab

243 """Get the current shell name, if available. 

244 

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 

258 

259 return name 1cdefghab