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

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

7 

8import click 1defghiabc

9from typer.core import HAS_SHELLINGHAM 1defghiabc

10 

11if HAS_SHELLINGHAM: 1defghiabc

12 import shellingham 1defghiabc

13 

14 

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

21 

22 

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} 

31 

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

33""" 

34 

35COMPLETION_SCRIPT_ZSH = """ 1defghiabc

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"' 1defghiabc

46 

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

68 

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} 

76 

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

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

79 

80 

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

95 

96 

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

121 

122 

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

149 

150 

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

160 

161 

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

203 

204 

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

240 

241 

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

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

258 

259 return name 1defghiabc