Coverage for typer / _completion_shared.py: 100%

111 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-09 00:15 +0000

1import os 1defgabc

2import re 1defgabc

3import subprocess 1defgabc

4from enum import Enum 1defgabc

5from pathlib import Path 1defgabc

6 

7import shellingham 1defgabc

8 

9from . import _click 1defgabc

10from ._click.globals import get_current_context 1defgabc

11from .exceptions import Exit 1defgabc

12 

13 

14class Shells(str, Enum): 1defgabc

15 bash = "bash" 1defgabc

16 zsh = "zsh" 1defgabc

17 fish = "fish" 1defgabc

18 powershell = "powershell" 1defgabc

19 pwsh = "pwsh" 1defgabc

20 

21 

22COMPLETION_SCRIPT_BASH = """ 1defgabc

23%(complete_func)s() { 

24 local IFS=$'\n' 

25 COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ 

26 COMP_CWORD=$COMP_CWORD \\ 

27 %(autocomplete_var)s=complete_bash $1 ) ) 

28 return 0 

29} 

30 

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

32""" 

33 

34COMPLETION_SCRIPT_ZSH = """ 1defgabc

35#compdef %(prog_name)s 

36 

37%(complete_func)s() { 

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

39} 

40 

41compdef %(complete_func)s %(prog_name)s 

42""" 

43 

44COMPLETION_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"' 1defgabc

45 

46COMPLETION_SCRIPT_POWER_SHELL = """ 1defgabc

47Import-Module PSReadLine 

48Set-PSReadLineKeyHandler -Chord Tab -Function MenuComplete 

49$scriptblock = { 

50 param($wordToComplete, $commandAst, $cursorPosition) 

51 $Env:%(autocomplete_var)s = "complete_powershell" 

52 $Env:_TYPER_COMPLETE_ARGS = $commandAst.ToString() 

53 $Env:_TYPER_COMPLETE_WORD_TO_COMPLETE = $wordToComplete 

54 %(prog_name)s | ForEach-Object { 

55 $commandArray = $_ -Split ":::" 

56 $command = $commandArray[0] 

57 $helpString = $commandArray[1] 

58 [System.Management.Automation.CompletionResult]::new( 

59 $command, $command, 'ParameterValue', $helpString) 

60 } 

61 $Env:%(autocomplete_var)s = "" 

62 $Env:_TYPER_COMPLETE_ARGS = "" 

63 $Env:_TYPER_COMPLETE_WORD_TO_COMPLETE = "" 

64} 

65Register-ArgumentCompleter -Native -CommandName %(prog_name)s -ScriptBlock $scriptblock 

66""" 

67 

68_completion_scripts = { 1defgabc

69 "bash": COMPLETION_SCRIPT_BASH, 

70 "zsh": COMPLETION_SCRIPT_ZSH, 

71 "fish": COMPLETION_SCRIPT_FISH, 

72 "powershell": COMPLETION_SCRIPT_POWER_SHELL, 

73 "pwsh": COMPLETION_SCRIPT_POWER_SHELL, 

74} 

75 

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

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

78 

79 

80def get_completion_script(*, prog_name: str, complete_var: str, shell: str) -> str: 1defgabc

81 cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_")) 1defgabc

82 script = _completion_scripts.get(shell) 1defgabc

83 if script is None: 1defgabc

84 _click.echo(f"Shell {shell} not supported.", err=True) 1defgabc

85 raise Exit(1) 1defgabc

86 return ( 1defgabc

87 script 

88 % { 

89 "complete_func": f"_{cf_name}_completion", 

90 "prog_name": prog_name, 

91 "autocomplete_var": complete_var, 

92 } 

93 ).strip() 

94 

95 

96def install_bash(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defgabc

97 # Ref: https://github.com/scop/bash-completion#faq 

98 # It seems bash-completion is the official completion system for bash: 

99 # Ref: https://www.gnu.org/software/bash/manual/html_node/A-Programmable-Completion-Example.html 

100 # But installing in the locations from the docs doesn't seem to have effect 

101 completion_path = Path.home() / ".bash_completions" / f"{prog_name}.sh" 1defgabc

102 rc_path = Path.home() / ".bashrc" 1defgabc

103 rc_path.parent.mkdir(parents=True, exist_ok=True) 1defgabc

104 rc_content = "" 1defgabc

105 if rc_path.is_file(): 1defgabc

106 rc_content = rc_path.read_text() 1deabc

107 completion_init_lines = [f"source '{completion_path}'"] 1defgabc

108 for line in completion_init_lines: 1defgabc

109 if line not in rc_content: # pragma: no cover 1defgabc

110 rc_content += f"\n{line}" 1defgabc

111 rc_content += "\n" 1defgabc

112 rc_path.write_text(rc_content) 1defgabc

113 # Install completion 

114 completion_path.parent.mkdir(parents=True, exist_ok=True) 1defgabc

115 script_content = get_completion_script( 1defgabc

116 prog_name=prog_name, complete_var=complete_var, shell=shell 

117 ) 

118 completion_path.write_text(script_content) 1defgabc

119 return completion_path 1defgabc

120 

121 

122def install_zsh(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defgabc

123 # Setup Zsh and load ~/.zfunc 

124 zshrc_path = Path.home() / ".zshrc" 1defgabc

125 zshrc_path.parent.mkdir(parents=True, exist_ok=True) 1defgabc

126 zshrc_content = "" 1defgabc

127 if zshrc_path.is_file(): 1defgabc

128 zshrc_content = zshrc_path.read_text() 1defgabc

129 completion_line = "fpath+=~/.zfunc; autoload -Uz compinit; compinit" 1defgabc

130 if completion_line not in zshrc_content: 1defgabc

131 zshrc_content += f"\n{completion_line}\n" 1defgabc

132 style_line = "zstyle ':completion:*' menu select" 1defgabc

133 # TODO: consider setting the style only for the current program 

134 # style_line = f"zstyle ':completion:*:*:{prog_name}:*' menu select" 

135 # Install zstyle completion config only if the user doesn't have a customization 

136 if "zstyle" not in zshrc_content: 1defgabc

137 zshrc_content += f"\n{style_line}\n" 1defgabc

138 zshrc_content = f"{zshrc_content.strip()}\n" 1defgabc

139 zshrc_path.write_text(zshrc_content) 1defgabc

140 # Install completion under ~/.zfunc/ 

141 path_obj = Path.home() / f".zfunc/_{prog_name}" 1defgabc

142 path_obj.parent.mkdir(parents=True, exist_ok=True) 1defgabc

143 script_content = get_completion_script( 1defgabc

144 prog_name=prog_name, complete_var=complete_var, shell=shell 

145 ) 

146 path_obj.write_text(script_content) 1defgabc

147 return path_obj 1defgabc

148 

149 

150def install_fish(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defgabc

151 path_obj = Path.home() / f".config/fish/completions/{prog_name}.fish" 1defgabc

152 parent_dir: Path = path_obj.parent 1defgabc

153 parent_dir.mkdir(parents=True, exist_ok=True) 1defgabc

154 script_content = get_completion_script( 1defgabc

155 prog_name=prog_name, complete_var=complete_var, shell=shell 

156 ) 

157 path_obj.write_text(f"{script_content}\n") 1defgabc

158 return path_obj 1defgabc

159 

160 

161def install_powershell(*, prog_name: str, complete_var: str, shell: str) -> Path: 1defgabc

162 subprocess.run( 1defgabc

163 [ 

164 shell, 

165 "-Command", 

166 "Set-ExecutionPolicy", 

167 "Unrestricted", 

168 "-Scope", 

169 "CurrentUser", 

170 ] 

171 ) 

172 result = subprocess.run( 1defgabc

173 [shell, "-NoProfile", "-Command", "echo", "$profile"], 

174 check=True, 

175 stdout=subprocess.PIPE, 

176 ) 

177 if result.returncode != 0: # pragma: no cover 1defgabc

178 _click.echo("Couldn't get PowerShell user profile", err=True) 

179 raise Exit(result.returncode) 

180 path_str = "" 1defgabc

181 if isinstance(result.stdout, str): # pragma: no cover 1defgabc

182 path_str = result.stdout 

183 if isinstance(result.stdout, bytes): 1defgabc

184 for encoding in ["windows-1252", "utf8", "cp850"]: 1defgabc

185 try: 1defgabc

186 path_str = result.stdout.decode(encoding) 1defgabc

187 break 1defgabc

188 except UnicodeDecodeError: # pragma: no cover 

189 pass 

190 if not path_str: # pragma: no cover 1defgabc

191 _click.echo("Couldn't decode the path automatically", err=True) 

192 raise Exit(1) 

193 path_obj = Path(path_str.strip()) 1defgabc

194 parent_dir: Path = path_obj.parent 1defgabc

195 parent_dir.mkdir(parents=True, exist_ok=True) 1defgabc

196 script_content = get_completion_script( 1defgabc

197 prog_name=prog_name, complete_var=complete_var, shell=shell 

198 ) 

199 with path_obj.open(mode="a") as f: 1defgabc

200 f.write(f"{script_content}\n") 1defgabc

201 return path_obj 1defgabc

202 

203 

204def install( 1defgabc

205 shell: str | None = None, 

206 prog_name: str | None = None, 

207 complete_var: str | None = None, 

208) -> tuple[str, Path]: 

209 prog_name = prog_name or get_current_context().find_root().info_name 1defgabc

210 assert prog_name 1defgabc

211 if complete_var is None: 1defgabc

212 complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper()) 1defgabc

213 test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION") 1defgabc

214 if shell is None and not test_disable_detection: 1defgabc

215 shell = _get_shell_name() 1defgabc

216 if shell == "bash": 1defgabc

217 installed_path = install_bash( 1defgabc

218 prog_name=prog_name, complete_var=complete_var, shell=shell 

219 ) 

220 return shell, installed_path 1defgabc

221 elif shell == "zsh": 1defgabc

222 installed_path = install_zsh( 1defgabc

223 prog_name=prog_name, complete_var=complete_var, shell=shell 

224 ) 

225 return shell, installed_path 1defgabc

226 elif shell == "fish": 1defgabc

227 installed_path = install_fish( 1defgabc

228 prog_name=prog_name, complete_var=complete_var, shell=shell 

229 ) 

230 return shell, installed_path 1defgabc

231 elif shell in {"powershell", "pwsh"}: 1defgabc

232 installed_path = install_powershell( 1defgabc

233 prog_name=prog_name, complete_var=complete_var, shell=shell 

234 ) 

235 return shell, installed_path 1defgabc

236 else: 

237 _click.echo(f"Shell {shell} is not supported.") 1defgabc

238 raise Exit(1) 1defgabc

239 

240 

241def _get_shell_name() -> str | None: 1defgabc

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

243 

244 The name will always be lowercase. If the shell cannot be detected, None is 

245 returned. 

246 """ 

247 name: str | None # N.B. shellingham is untyped 

248 try: 1defgabc

249 # N.B. detect_shell returns a tuple of (shell name, shell command). 

250 # We only need the name. 

251 name, _cmd = shellingham.detect_shell() # noqa: TID251 1defgabc

252 except shellingham.ShellDetectionFailure: # pragma: no cover 1abc

253 name = None 1abc

254 

255 return name 1defgabc