1- // PowerShell completions are based on the amazing work from clap:
2- // https://github.com/clap-rs/clap/blob/3294d18efe5f264d12c9035f404c7d189d4824e1/src/completions/powershell.rs
3- //
41// The generated scripts require PowerShell v5.0+ (which comes Windows 10, but
52// can be downloaded separately for windows 7 or 8.1).
63
@@ -11,90 +8,278 @@ import (
118 "fmt"
129 "io"
1310 "os"
14- "strings"
15-
16- "github.com/spf13/pflag"
1711)
1812
19- var powerShellCompletionTemplate = `using namespace System.Management.Automation
20- using namespace System.Management.Automation.Language
21- Register-ArgumentCompleter -Native -CommandName '%s' -ScriptBlock {
22- param($wordToComplete, $commandAst, $cursorPosition)
23- $commandElements = $commandAst.CommandElements
24- $command = @(
25- '%s'
26- for ($i = 1; $i -lt $commandElements.Count; $i++) {
27- $element = $commandElements[$i]
28- if ($element -isnot [StringConstantExpressionAst] -or
29- $element.StringConstantType -ne [StringConstantType]::BareWord -or
30- $element.Value.StartsWith('-')) {
31- break
32- }
33- $element.Value
34- }
35- ) -join ';'
36- $completions = @(switch ($command) {%s
37- })
38- $completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
39- Sort-Object -Property ListItemText
40- }`
41-
42- func generatePowerShellSubcommandCases (out io.Writer , cmd * Command , previousCommandName string ) {
43- var cmdName string
44- if previousCommandName == "" {
45- cmdName = cmd .Name ()
46- } else {
47- cmdName = fmt .Sprintf ("%s;%s" , previousCommandName , cmd .Name ())
48- }
49-
50- fmt .Fprintf (out , "\n '%s' {" , cmdName )
51-
52- cmd .Flags ().VisitAll (func (flag * pflag.Flag ) {
53- if nonCompletableFlag (flag ) {
54- return
55- }
56- usage := escapeStringForPowerShell (flag .Usage )
57- if len (flag .Shorthand ) > 0 {
58- fmt .Fprintf (out , "\n [CompletionResult]::new('-%s', '%s', [CompletionResultType]::ParameterName, '%s')" , flag .Shorthand , flag .Shorthand , usage )
59- }
60- fmt .Fprintf (out , "\n [CompletionResult]::new('--%s', '%s', [CompletionResultType]::ParameterName, '%s')" , flag .Name , flag .Name , usage )
61- })
62-
63- for _ , subCmd := range cmd .Commands () {
64- usage := escapeStringForPowerShell (subCmd .Short )
65- fmt .Fprintf (out , "\n [CompletionResult]::new('%s', '%s', [CompletionResultType]::ParameterValue, '%s')" , subCmd .Name (), subCmd .Name (), usage )
13+ func genPowerShellComp (buf * bytes.Buffer , name string , includeDesc bool ) {
14+ compCmd := ShellCompRequestCmd
15+ if ! includeDesc {
16+ compCmd = ShellCompNoDescRequestCmd
6617 }
18+ buf .WriteString (fmt .Sprintf (`# powershell completion for %-36[1]s -*- shell-script -*-
6719
68- fmt .Fprint (out , "\n break\n }" )
69-
70- for _ , subCmd := range cmd .Commands () {
71- generatePowerShellSubcommandCases (out , subCmd , cmdName )
72- }
20+ function __%[1]s_debug {
21+ if ($env:BASH_COMP_DEBUG_FILE) {
22+ "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE"
23+ }
7324}
7425
75- func escapeStringForPowerShell ( s string ) string {
76- return strings . Replace ( s , "'" , "''" , - 1 )
26+ filter __%[1]s_escapeStringWithSpecialChars {
27+ ` + " $_ -replace ' \\ s|#|@| \\ $|;|,|''| \\ {| \\ }| \\ (| \\ )| \" |`| \\ ||<|>|&','`$&'" + `
7728}
7829
79- // GenPowerShellCompletion generates PowerShell completion file and writes to the passed writer.
80- func (c * Command ) GenPowerShellCompletion (w io.Writer ) error {
81- buf := new (bytes.Buffer )
30+ Register-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 $ShellCompDirectiveNoFileComp) -ne 0 ) {
144+ __%[1]s_debug "ShellCompDirectiveNoFileComp is called"
145+
146+ if ($Values.Length -eq 0) {
147+ # Just print an empty string here so the
148+ # shell does not start to complete paths.
149+ # We cannot use CompletionResult here because
150+ # it does not accept an empty string as argument.
151+ ""
152+ return
153+ }
154+ }
155+
156+ if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or
157+ (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) {
158+ __%[1]s_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported"
159+
160+ # return here to prevent the completion of the extensions
161+ return
162+ }
163+
164+ $Values = $Values | Where-Object {
165+ # filter the result
166+ $_.Name -like "$WordToComplete*"
82167
83- var subCommandCases bytes.Buffer
84- generatePowerShellSubcommandCases (& subCommandCases , c , "" )
85- fmt .Fprintf (buf , powerShellCompletionTemplate , c .Name (), c .Name (), subCommandCases .String ())
168+ # Join the flag back if we have a equal sign flag
169+ if ( $IsEqualFlag ) {
170+ __%[1]s_debug "Join the equal sign flag back to the completion value"
171+ $_.Name = $Flag + "=" + $_.Name
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 temporay 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"
86203
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+
248+ func (c * Command ) genPowerShellCompletion (w io.Writer , includeDesc bool ) error {
249+ buf := new (bytes.Buffer )
250+ genPowerShellComp (buf , c .Name (), includeDesc )
87251 _ , err := buf .WriteTo (w )
88252 return err
89253}
90254
91- // GenPowerShellCompletionFile generates PowerShell completion file.
92- func (c * Command ) GenPowerShellCompletionFile (filename string ) error {
255+ func (c * Command ) genPowerShellCompletionFile (filename string , includeDesc bool ) error {
93256 outFile , err := os .Create (filename )
94257 if err != nil {
95258 return err
96259 }
97260 defer outFile .Close ()
98261
99- return c .GenPowerShellCompletion (outFile )
262+ return c .genPowerShellCompletion (outFile , includeDesc )
263+ }
264+
265+ // GenPowerShellCompletionFile generates powershell completion file without descriptions.
266+ func (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.
272+ func (c * Command ) GenPowerShellCompletion (w io.Writer ) error {
273+ return c .genPowerShellCompletion (w , false )
274+ }
275+
276+ // GenPowerShellCompletionFileWithDesc generates powershell completion file with descriptions.
277+ func (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.
283+ func (c * Command ) GenPowerShellCompletionWithDesc (w io.Writer ) error {
284+ return c .genPowerShellCompletion (w , true )
100285}
0 commit comments