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