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