powershell_completions.go

  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}