| Abhay Kumar | a61c522 | 2025-11-10 07:32:50 +0000 | [diff] [blame^] | 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.ReplaceAll(nameForVar, "-", "_") |
| 32 | nameForVar = strings.ReplaceAll(nameForVar, ":", "_") |
| 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 | New-Object -TypeName PSCustomObject -Property @{ |
| 166 | Name = "$Name" |
| 167 | Description = "$Description" |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | |
| 172 | $Space = " " |
| 173 | if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) { |
| 174 | # remove the space here |
| 175 | __%[1]s_debug "ShellCompDirectiveNoSpace is called" |
| 176 | $Space = "" |
| 177 | } |
| 178 | |
| 179 | if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or |
| 180 | (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) { |
| 181 | __%[1]s_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported" |
| 182 | |
| 183 | # return here to prevent the completion of the extensions |
| 184 | return |
| 185 | } |
| 186 | |
| 187 | $Values = $Values | Where-Object { |
| 188 | # filter the result |
| 189 | $_.Name -like "$WordToComplete*" |
| 190 | |
| 191 | # Join the flag back if we have an equal sign flag |
| 192 | if ( $IsEqualFlag ) { |
| 193 | __%[1]s_debug "Join the equal sign flag back to the completion value" |
| 194 | $_.Name = $Flag + "=" + $_.Name |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | # we sort the values in ascending order by name if keep order isn't passed |
| 199 | if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) { |
| 200 | $Values = $Values | Sort-Object -Property Name |
| 201 | } |
| 202 | |
| 203 | if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) { |
| 204 | __%[1]s_debug "ShellCompDirectiveNoFileComp is called" |
| 205 | |
| 206 | if ($Values.Length -eq 0) { |
| 207 | # Just print an empty string here so the |
| 208 | # shell does not start to complete paths. |
| 209 | # We cannot use CompletionResult here because |
| 210 | # it does not accept an empty string as argument. |
| 211 | "" |
| 212 | return |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | # Get the current mode |
| 217 | $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function |
| 218 | __%[1]s_debug "Mode: $Mode" |
| 219 | |
| 220 | $Values | ForEach-Object { |
| 221 | |
| 222 | # store temporary because switch will overwrite $_ |
| 223 | $comp = $_ |
| 224 | |
| 225 | # PowerShell supports three different completion modes |
| 226 | # - TabCompleteNext (default windows style - on each key press the next option is displayed) |
| 227 | # - Complete (works like bash) |
| 228 | # - MenuComplete (works like zsh) |
| 229 | # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function <mode> |
| 230 | |
| 231 | # CompletionResult Arguments: |
| 232 | # 1) CompletionText text to be used as the auto completion result |
| 233 | # 2) ListItemText text to be displayed in the suggestion list |
| 234 | # 3) ResultType type of completion result |
| 235 | # 4) ToolTip text for the tooltip with details about the object |
| 236 | |
| 237 | switch ($Mode) { |
| 238 | |
| 239 | # bash like |
| 240 | "Complete" { |
| 241 | |
| 242 | if ($Values.Length -eq 1) { |
| 243 | __%[1]s_debug "Only one completion left" |
| 244 | |
| 245 | # insert space after value |
| 246 | $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space |
| 247 | if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ |
| 248 | [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") |
| 249 | } else { |
| 250 | $CompletionText |
| 251 | } |
| 252 | |
| 253 | } else { |
| 254 | # Add the proper number of spaces to align the descriptions |
| 255 | while($comp.Name.Length -lt $Longest) { |
| 256 | $comp.Name = $comp.Name + " " |
| 257 | } |
| 258 | |
| 259 | # Check for empty description and only add parentheses if needed |
| 260 | if ($($comp.Description) -eq " " ) { |
| 261 | $Description = "" |
| 262 | } else { |
| 263 | $Description = " ($($comp.Description))" |
| 264 | } |
| 265 | |
| 266 | $CompletionText = "$($comp.Name)$Description" |
| 267 | if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ |
| 268 | [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") |
| 269 | } else { |
| 270 | $CompletionText |
| 271 | } |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | # zsh like |
| 276 | "MenuComplete" { |
| 277 | # insert space after value |
| 278 | # MenuComplete will automatically show the ToolTip of |
| 279 | # the highlighted value at the bottom of the suggestions. |
| 280 | |
| 281 | $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space |
| 282 | if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ |
| 283 | [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") |
| 284 | } else { |
| 285 | $CompletionText |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | # TabCompleteNext and in case we get something unknown |
| 290 | Default { |
| 291 | # Like MenuComplete but we don't want to add a space here because |
| 292 | # the user need to press space anyway to get the completion. |
| 293 | # Description will not be shown because that's not possible with TabCompleteNext |
| 294 | |
| 295 | $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) |
| 296 | if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ |
| 297 | [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") |
| 298 | } else { |
| 299 | $CompletionText |
| 300 | } |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock ${__%[2]sCompleterBlock} |
| 308 | `, name, nameForVar, compCmd, |
| 309 | ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
| 310 | ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, activeHelpEnvVar(name))) |
| 311 | } |
| 312 | |
| 313 | func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error { |
| 314 | buf := new(bytes.Buffer) |
| 315 | genPowerShellComp(buf, c.Name(), includeDesc) |
| 316 | _, err := buf.WriteTo(w) |
| 317 | return err |
| 318 | } |
| 319 | |
| 320 | func (c *Command) genPowerShellCompletionFile(filename string, includeDesc bool) error { |
| 321 | outFile, err := os.Create(filename) |
| 322 | if err != nil { |
| 323 | return err |
| 324 | } |
| 325 | defer outFile.Close() |
| 326 | |
| 327 | return c.genPowerShellCompletion(outFile, includeDesc) |
| 328 | } |
| 329 | |
| 330 | // GenPowerShellCompletionFile generates powershell completion file without descriptions. |
| 331 | func (c *Command) GenPowerShellCompletionFile(filename string) error { |
| 332 | return c.genPowerShellCompletionFile(filename, false) |
| 333 | } |
| 334 | |
| 335 | // GenPowerShellCompletion generates powershell completion file without descriptions |
| 336 | // and writes it to the passed writer. |
| 337 | func (c *Command) GenPowerShellCompletion(w io.Writer) error { |
| 338 | return c.genPowerShellCompletion(w, false) |
| 339 | } |
| 340 | |
| 341 | // GenPowerShellCompletionFileWithDesc generates powershell completion file with descriptions. |
| 342 | func (c *Command) GenPowerShellCompletionFileWithDesc(filename string) error { |
| 343 | return c.genPowerShellCompletionFile(filename, true) |
| 344 | } |
| 345 | |
| 346 | // GenPowerShellCompletionWithDesc generates powershell completion file with descriptions |
| 347 | // and writes it to the passed writer. |
| 348 | func (c *Command) GenPowerShellCompletionWithDesc(w io.Writer) error { |
| 349 | return c.genPowerShellCompletion(w, true) |
| 350 | } |