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
18package cobra
19
20import (
21 "bytes"
22 "fmt"
23 "io"
24 "os"
25 "strings"
26)
27
28func 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
40function __%[1]s_debug {
41 if ($env:BASH_COMP_DEBUG_FILE) {
42 "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE"
43 }
44}
45
46filter __%[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
307Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock ${__%[2]sCompleterBlock}
308`, name, nameForVar, compCmd,
309 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
310 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, activeHelpEnvVar(name)))
311}
312
313func (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
320func (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.
331func (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.
337func (c *Command) GenPowerShellCompletion(w io.Writer) error {
338 return c.genPowerShellCompletion(w, false)
339}
340
341// GenPowerShellCompletionFileWithDesc generates powershell completion file with descriptions.
342func (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.
348func (c *Command) GenPowerShellCompletionWithDesc(w io.Writer) error {
349 return c.genPowerShellCompletion(w, true)
350}