git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cobra/powershell_completions.go (about) 1 // Copyright 2013-2022 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 81 # Prepare the command to request completions for the program. 82 # Split the command at the first space to separate the program and arguments. 83 $Program,$Arguments = $Command.Split(" ",2) 84 85 $RequestComp="$Program %[3]s $Arguments" 86 __%[1]s_debug "RequestComp: $RequestComp" 87 88 # we cannot use $WordToComplete because it 89 # has the wrong values if the cursor was moved 90 # so use the last argument 91 if ($WordToComplete -ne "" ) { 92 $WordToComplete = $Arguments.Split(" ")[-1] 93 } 94 __%[1]s_debug "New WordToComplete: $WordToComplete" 95 96 97 # Check for flag with equal sign 98 $IsEqualFlag = ($WordToComplete -Like "--*=*" ) 99 if ( $IsEqualFlag ) { 100 __%[1]s_debug "Completing equal sign flag" 101 # Remove the flag part 102 $Flag,$WordToComplete = $WordToComplete.Split("=",2) 103 } 104 105 if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) { 106 # If the last parameter is complete (there is a space following it) 107 # We add an extra empty parameter so we can indicate this to the go method. 108 __%[1]s_debug "Adding extra empty parameter" 109 `+" # We need to use `\"`\" to pass an empty argument a \"\" or '' does not work!!!"+` 110 `+" $RequestComp=\"$RequestComp\" + ' `\"`\"'"+` 111 } 112 113 __%[1]s_debug "Calling $RequestComp" 114 # First disable ActiveHelp which is not supported for Powershell 115 $env:%[9]s=0 116 117 #call the command store the output in $out and redirect stderr and stdout to null 118 # $Out is an array contains each line per element 119 Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null 120 121 # get directive from last line 122 [int]$Directive = $Out[-1].TrimStart(':') 123 if ($Directive -eq "") { 124 # There is no directive specified 125 $Directive = 0 126 } 127 __%[1]s_debug "The completion directive is: $Directive" 128 129 # remove directive (last element) from out 130 $Out = $Out | Where-Object { $_ -ne $Out[-1] } 131 __%[1]s_debug "The completions are: $Out" 132 133 if (($Directive -band $ShellCompDirectiveError) -ne 0 ) { 134 # Error code. No completion. 135 __%[1]s_debug "Received error from custom completion go code" 136 return 137 } 138 139 $Longest = 0 140 $Values = $Out | ForEach-Object { 141 #Split the output in name and description 142 `+" $Name, $Description = $_.Split(\"`t\",2)"+` 143 __%[1]s_debug "Name: $Name Description: $Description" 144 145 # Look for the longest completion so that we can format things nicely 146 if ($Longest -lt $Name.Length) { 147 $Longest = $Name.Length 148 } 149 150 # Set the description to a one space string if there is none set. 151 # This is needed because the CompletionResult does not accept an empty string as argument 152 if (-Not $Description) { 153 $Description = " " 154 } 155 @{Name="$Name";Description="$Description"} 156 } 157 158 159 $Space = " " 160 if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) { 161 # remove the space here 162 __%[1]s_debug "ShellCompDirectiveNoSpace is called" 163 $Space = "" 164 } 165 166 if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or 167 (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) { 168 __%[1]s_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported" 169 170 # return here to prevent the completion of the extensions 171 return 172 } 173 174 $Values = $Values | Where-Object { 175 # filter the result 176 $_.Name -like "$WordToComplete*" 177 178 # Join the flag back if we have an equal sign flag 179 if ( $IsEqualFlag ) { 180 __%[1]s_debug "Join the equal sign flag back to the completion value" 181 $_.Name = $Flag + "=" + $_.Name 182 } 183 } 184 185 if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) { 186 __%[1]s_debug "ShellCompDirectiveNoFileComp is called" 187 188 if ($Values.Length -eq 0) { 189 # Just print an empty string here so the 190 # shell does not start to complete paths. 191 # We cannot use CompletionResult here because 192 # it does not accept an empty string as argument. 193 "" 194 return 195 } 196 } 197 198 # Get the current mode 199 $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function 200 __%[1]s_debug "Mode: $Mode" 201 202 $Values | ForEach-Object { 203 204 # store temporary because switch will overwrite $_ 205 $comp = $_ 206 207 # PowerShell supports three different completion modes 208 # - TabCompleteNext (default windows style - on each key press the next option is displayed) 209 # - Complete (works like bash) 210 # - MenuComplete (works like zsh) 211 # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function <mode> 212 213 # CompletionResult Arguments: 214 # 1) CompletionText text to be used as the auto completion result 215 # 2) ListItemText text to be displayed in the suggestion list 216 # 3) ResultType type of completion result 217 # 4) ToolTip text for the tooltip with details about the object 218 219 switch ($Mode) { 220 221 # bash like 222 "Complete" { 223 224 if ($Values.Length -eq 1) { 225 __%[1]s_debug "Only one completion left" 226 227 # insert space after value 228 [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 229 230 } else { 231 # Add the proper number of spaces to align the descriptions 232 while($comp.Name.Length -lt $Longest) { 233 $comp.Name = $comp.Name + " " 234 } 235 236 # Check for empty description and only add parentheses if needed 237 if ($($comp.Description) -eq " " ) { 238 $Description = "" 239 } else { 240 $Description = " ($($comp.Description))" 241 } 242 243 [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") 244 } 245 } 246 247 # zsh like 248 "MenuComplete" { 249 # insert space after value 250 # MenuComplete will automatically show the ToolTip of 251 # the highlighted value at the bottom of the suggestions. 252 [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 253 } 254 255 # TabCompleteNext and in case we get something unknown 256 Default { 257 # Like MenuComplete but we don't want to add a space here because 258 # the user need to press space anyway to get the completion. 259 # Description will not be shown because that's not possible with TabCompleteNext 260 [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 261 } 262 } 263 264 } 265 } 266 267 Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock $__%[2]sCompleterBlock 268 `, name, nameForVar, compCmd, 269 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, 270 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name))) 271 } 272 273 func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error { 274 buf := new(bytes.Buffer) 275 genPowerShellComp(buf, c.Name(), includeDesc) 276 _, err := buf.WriteTo(w) 277 return err 278 } 279 280 func (c *Command) genPowerShellCompletionFile(filename string, includeDesc bool) error { 281 outFile, err := os.Create(filename) 282 if err != nil { 283 return err 284 } 285 defer outFile.Close() 286 287 return c.genPowerShellCompletion(outFile, includeDesc) 288 } 289 290 // GenPowerShellCompletionFile generates powershell completion file without descriptions. 291 func (c *Command) GenPowerShellCompletionFile(filename string) error { 292 return c.genPowerShellCompletionFile(filename, false) 293 } 294 295 // GenPowerShellCompletion generates powershell completion file without descriptions 296 // and writes it to the passed writer. 297 func (c *Command) GenPowerShellCompletion(w io.Writer) error { 298 return c.genPowerShellCompletion(w, false) 299 } 300 301 // GenPowerShellCompletionFileWithDesc generates powershell completion file with descriptions. 302 func (c *Command) GenPowerShellCompletionFileWithDesc(filename string) error { 303 return c.genPowerShellCompletionFile(filename, true) 304 } 305 306 // GenPowerShellCompletionWithDesc generates powershell completion file with descriptions 307 // and writes it to the passed writer. 308 func (c *Command) GenPowerShellCompletionWithDesc(w io.Writer) error { 309 return c.genPowerShellCompletion(w, true) 310 }