powershell_completions.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. // Copyright 2013-2023 The Cobra Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. // The generated scripts require PowerShell v5.0+ (which comes Windows 10, but
  15. // can be downloaded separately for windows 7 or 8.1).
  16. package cobra
  17. import (
  18. "bytes"
  19. "fmt"
  20. "io"
  21. "os"
  22. "strings"
  23. )
  24. func genPowerShellComp(buf io.StringWriter, name string, includeDesc bool) {
  25. // Variables should not contain a '-' or ':' character
  26. nameForVar := name
  27. nameForVar = strings.ReplaceAll(nameForVar, "-", "_")
  28. nameForVar = strings.ReplaceAll(nameForVar, ":", "_")
  29. compCmd := ShellCompRequestCmd
  30. if !includeDesc {
  31. compCmd = ShellCompNoDescRequestCmd
  32. }
  33. WriteStringAndCheck(buf, fmt.Sprintf(`# powershell completion for %-36[1]s -*- shell-script -*-
  34. function __%[1]s_debug {
  35. if ($env:BASH_COMP_DEBUG_FILE) {
  36. "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE"
  37. }
  38. }
  39. filter __%[1]s_escapeStringWithSpecialChars {
  40. `+" $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|\"|`|\\||<|>|&','`$&'"+`
  41. }
  42. [scriptblock]${__%[2]sCompleterBlock} = {
  43. param(
  44. $WordToComplete,
  45. $CommandAst,
  46. $CursorPosition
  47. )
  48. # Get the current command line and convert into a string
  49. $Command = $CommandAst.CommandElements
  50. $Command = "$Command"
  51. __%[1]s_debug ""
  52. __%[1]s_debug "========= starting completion logic =========="
  53. __%[1]s_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition"
  54. # The user could have moved the cursor backwards on the command-line.
  55. # We need to trigger completion from the $CursorPosition location, so we need
  56. # to truncate the command-line ($Command) up to the $CursorPosition location.
  57. # Make sure the $Command is longer then the $CursorPosition before we truncate.
  58. # This happens because the $Command does not include the last space.
  59. if ($Command.Length -gt $CursorPosition) {
  60. $Command=$Command.Substring(0,$CursorPosition)
  61. }
  62. __%[1]s_debug "Truncated command: $Command"
  63. $ShellCompDirectiveError=%[4]d
  64. $ShellCompDirectiveNoSpace=%[5]d
  65. $ShellCompDirectiveNoFileComp=%[6]d
  66. $ShellCompDirectiveFilterFileExt=%[7]d
  67. $ShellCompDirectiveFilterDirs=%[8]d
  68. $ShellCompDirectiveKeepOrder=%[9]d
  69. # Prepare the command to request completions for the program.
  70. # Split the command at the first space to separate the program and arguments.
  71. $Program,$Arguments = $Command.Split(" ",2)
  72. $RequestComp="$Program %[3]s $Arguments"
  73. __%[1]s_debug "RequestComp: $RequestComp"
  74. # we cannot use $WordToComplete because it
  75. # has the wrong values if the cursor was moved
  76. # so use the last argument
  77. if ($WordToComplete -ne "" ) {
  78. $WordToComplete = $Arguments.Split(" ")[-1]
  79. }
  80. __%[1]s_debug "New WordToComplete: $WordToComplete"
  81. # Check for flag with equal sign
  82. $IsEqualFlag = ($WordToComplete -Like "--*=*" )
  83. if ( $IsEqualFlag ) {
  84. __%[1]s_debug "Completing equal sign flag"
  85. # Remove the flag part
  86. $Flag,$WordToComplete = $WordToComplete.Split("=",2)
  87. }
  88. if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) {
  89. # If the last parameter is complete (there is a space following it)
  90. # We add an extra empty parameter so we can indicate this to the go method.
  91. __%[1]s_debug "Adding extra empty parameter"
  92. # PowerShell 7.2+ changed the way how the arguments are passed to executables,
  93. # so for pre-7.2 or when Legacy argument passing is enabled we need to use
  94. `+" # `\"`\" to pass an empty argument, a \"\" or '' does not work!!!"+`
  95. if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or
  96. ($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or
  97. (($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and
  98. $PSNativeCommandArgumentPassing -eq 'Legacy')) {
  99. `+" $RequestComp=\"$RequestComp\" + ' `\"`\"'"+`
  100. } else {
  101. $RequestComp="$RequestComp" + ' ""'
  102. }
  103. }
  104. __%[1]s_debug "Calling $RequestComp"
  105. # First disable ActiveHelp which is not supported for Powershell
  106. ${env:%[10]s}=0
  107. #call the command store the output in $out and redirect stderr and stdout to null
  108. # $Out is an array contains each line per element
  109. Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null
  110. # get directive from last line
  111. [int]$Directive = $Out[-1].TrimStart(':')
  112. if ($Directive -eq "") {
  113. # There is no directive specified
  114. $Directive = 0
  115. }
  116. __%[1]s_debug "The completion directive is: $Directive"
  117. # remove directive (last element) from out
  118. $Out = $Out | Where-Object { $_ -ne $Out[-1] }
  119. __%[1]s_debug "The completions are: $Out"
  120. if (($Directive -band $ShellCompDirectiveError) -ne 0 ) {
  121. # Error code. No completion.
  122. __%[1]s_debug "Received error from custom completion go code"
  123. return
  124. }
  125. $Longest = 0
  126. [Array]$Values = $Out | ForEach-Object {
  127. #Split the output in name and description
  128. `+" $Name, $Description = $_.Split(\"`t\",2)"+`
  129. __%[1]s_debug "Name: $Name Description: $Description"
  130. # Look for the longest completion so that we can format things nicely
  131. if ($Longest -lt $Name.Length) {
  132. $Longest = $Name.Length
  133. }
  134. # Set the description to a one space string if there is none set.
  135. # This is needed because the CompletionResult does not accept an empty string as argument
  136. if (-Not $Description) {
  137. $Description = " "
  138. }
  139. New-Object -TypeName PSCustomObject -Property @{
  140. Name = "$Name"
  141. Description = "$Description"
  142. }
  143. }
  144. $Space = " "
  145. if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) {
  146. # remove the space here
  147. __%[1]s_debug "ShellCompDirectiveNoSpace is called"
  148. $Space = ""
  149. }
  150. if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or
  151. (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) {
  152. __%[1]s_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported"
  153. # return here to prevent the completion of the extensions
  154. return
  155. }
  156. $Values = $Values | Where-Object {
  157. # filter the result
  158. $_.Name -like "$WordToComplete*"
  159. # Join the flag back if we have an equal sign flag
  160. if ( $IsEqualFlag ) {
  161. __%[1]s_debug "Join the equal sign flag back to the completion value"
  162. $_.Name = $Flag + "=" + $_.Name
  163. }
  164. }
  165. # we sort the values in ascending order by name if keep order isn't passed
  166. if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) {
  167. $Values = $Values | Sort-Object -Property Name
  168. }
  169. if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) {
  170. __%[1]s_debug "ShellCompDirectiveNoFileComp is called"
  171. if ($Values.Length -eq 0) {
  172. # Just print an empty string here so the
  173. # shell does not start to complete paths.
  174. # We cannot use CompletionResult here because
  175. # it does not accept an empty string as argument.
  176. ""
  177. return
  178. }
  179. }
  180. # Get the current mode
  181. $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function
  182. __%[1]s_debug "Mode: $Mode"
  183. $Values | ForEach-Object {
  184. # store temporary because switch will overwrite $_
  185. $comp = $_
  186. # PowerShell supports three different completion modes
  187. # - TabCompleteNext (default windows style - on each key press the next option is displayed)
  188. # - Complete (works like bash)
  189. # - MenuComplete (works like zsh)
  190. # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function <mode>
  191. # CompletionResult Arguments:
  192. # 1) CompletionText text to be used as the auto completion result
  193. # 2) ListItemText text to be displayed in the suggestion list
  194. # 3) ResultType type of completion result
  195. # 4) ToolTip text for the tooltip with details about the object
  196. switch ($Mode) {
  197. # bash like
  198. "Complete" {
  199. if ($Values.Length -eq 1) {
  200. __%[1]s_debug "Only one completion left"
  201. # insert space after value
  202. $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space
  203. if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){
  204. [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
  205. } else {
  206. $CompletionText
  207. }
  208. } else {
  209. # Add the proper number of spaces to align the descriptions
  210. while($comp.Name.Length -lt $Longest) {
  211. $comp.Name = $comp.Name + " "
  212. }
  213. # Check for empty description and only add parentheses if needed
  214. if ($($comp.Description) -eq " " ) {
  215. $Description = ""
  216. } else {
  217. $Description = " ($($comp.Description))"
  218. }
  219. $CompletionText = "$($comp.Name)$Description"
  220. if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){
  221. [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)")
  222. } else {
  223. $CompletionText
  224. }
  225. }
  226. }
  227. # zsh like
  228. "MenuComplete" {
  229. # insert space after value
  230. # MenuComplete will automatically show the ToolTip of
  231. # the highlighted value at the bottom of the suggestions.
  232. $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space
  233. if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){
  234. [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
  235. } else {
  236. $CompletionText
  237. }
  238. }
  239. # TabCompleteNext and in case we get something unknown
  240. Default {
  241. # Like MenuComplete but we don't want to add a space here because
  242. # the user need to press space anyway to get the completion.
  243. # Description will not be shown because that's not possible with TabCompleteNext
  244. $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars)
  245. if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){
  246. [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
  247. } else {
  248. $CompletionText
  249. }
  250. }
  251. }
  252. }
  253. }
  254. Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock ${__%[2]sCompleterBlock}
  255. `, name, nameForVar, compCmd,
  256. ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
  257. ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, activeHelpEnvVar(name)))
  258. }
  259. func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error {
  260. buf := new(bytes.Buffer)
  261. genPowerShellComp(buf, c.Name(), includeDesc)
  262. _, err := buf.WriteTo(w)
  263. return err
  264. }
  265. func (c *Command) genPowerShellCompletionFile(filename string, includeDesc bool) error {
  266. outFile, err := os.Create(filename)
  267. if err != nil {
  268. return err
  269. }
  270. defer outFile.Close()
  271. return c.genPowerShellCompletion(outFile, includeDesc)
  272. }
  273. // GenPowerShellCompletionFile generates powershell completion file without descriptions.
  274. func (c *Command) GenPowerShellCompletionFile(filename string) error {
  275. return c.genPowerShellCompletionFile(filename, false)
  276. }
  277. // GenPowerShellCompletion generates powershell completion file without descriptions
  278. // and writes it to the passed writer.
  279. func (c *Command) GenPowerShellCompletion(w io.Writer) error {
  280. return c.genPowerShellCompletion(w, false)
  281. }
  282. // GenPowerShellCompletionFileWithDesc generates powershell completion file with descriptions.
  283. func (c *Command) GenPowerShellCompletionFileWithDesc(filename string) error {
  284. return c.genPowerShellCompletionFile(filename, true)
  285. }
  286. // GenPowerShellCompletionWithDesc generates powershell completion file with descriptions
  287. // and writes it to the passed writer.
  288. func (c *Command) GenPowerShellCompletionWithDesc(w io.Writer) error {
  289. return c.genPowerShellCompletion(w, true)
  290. }