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

1import os 1cdefgab

2import re 1cdefgab

3import subprocess 1cdefgab

4from enum import Enum 1cdefgab

5from pathlib import Path 1cdefgab

6 

7import click 1cdefgab

8import shellingham 1cdefgab

9 

10 

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

17 

18 

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} 

27 

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

29""" 

30 

31COMPLETION_SCRIPT_ZSH = """ 1cdefgab

32#compdef %(prog_name)s 

33 

34%(complete_func)s() { 

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

36} 

37 

38compdef %(complete_func)s %(prog_name)s 

39""" 

40 

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

42 

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

64 

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} 

72 

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

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

75 

76 

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

91 

92 

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

117 

118 

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

145 

146 

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

156 

157 

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

199 

200 

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

236 

237 

238def _get_shell_name() -> str | None: 1cdefgab

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

240 

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

251 

252 return name 1cdefgab