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