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 // |
|
4 // The generated scripts require PowerShell v5.0+ (which comes Windows 10, but |
1 // The generated scripts require PowerShell v5.0+ (which comes Windows 10, but |
5 // can be downloaded separately for windows 7 or 8.1). |
2 // can be downloaded separately for windows 7 or 8.1). |
6 |
3 |
7 package cobra |
4 package cobra |
8 |
5 |
9 import ( |
6 import ( |
10 "bytes" |
7 "bytes" |
11 "fmt" |
8 "fmt" |
12 "io" |
9 "io" |
13 "os" |
10 "os" |
14 "strings" |
|
15 |
|
16 "github.com/spf13/pflag" |
|
17 ) |
11 ) |
18 |
12 |
19 var powerShellCompletionTemplate = `using namespace System.Management.Automation |
13 func genPowerShellComp(buf io.StringWriter, name string, includeDesc bool) { |
20 using namespace System.Management.Automation.Language |
14 compCmd := ShellCompRequestCmd |
21 Register-ArgumentCompleter -Native -CommandName '%s' -ScriptBlock { |
15 if !includeDesc { |
22 param($wordToComplete, $commandAst, $cursorPosition) |
16 compCmd = ShellCompNoDescRequestCmd |
23 $commandElements = $commandAst.CommandElements |
17 } |
24 $command = @( |
18 WriteStringAndCheck(buf, fmt.Sprintf(`# powershell completion for %-36[1]s -*- shell-script -*- |
25 '%s' |
19 |
26 for ($i = 1; $i -lt $commandElements.Count; $i++) { |
20 function __%[1]s_debug { |
27 $element = $commandElements[$i] |
21 if ($env:BASH_COMP_DEBUG_FILE) { |
28 if ($element -isnot [StringConstantExpressionAst] -or |
22 "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" |
29 $element.StringConstantType -ne [StringConstantType]::BareWord -or |
23 } |
30 $element.Value.StartsWith('-')) { |
24 } |
31 break |
25 |
|
26 filter __%[1]s_escapeStringWithSpecialChars { |
|
27 `+" $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|\"|`|\\||<|>|&','`$&'"+` |
|
28 } |
|
29 |
|
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 $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)") |
32 } |
230 } |
33 $element.Value |
231 |
34 } |
232 # TabCompleteNext and in case we get something unknown |
35 ) -join ';' |
233 Default { |
36 $completions = @(switch ($command) {%s |
234 # Like MenuComplete but we don't want to add a space here because |
37 }) |
235 # the user need to press space anyway to get the completion. |
38 $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | |
236 # Description will not be shown because thats not possible with TabCompleteNext |
39 Sort-Object -Property ListItemText |
237 [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") |
40 }` |
238 } |
41 |
239 } |
42 func generatePowerShellSubcommandCases(out io.Writer, cmd *Command, previousCommandName string) { |
240 |
43 var cmdName string |
241 } |
44 if previousCommandName == "" { |
242 } |
45 cmdName = cmd.Name() |
243 `, name, compCmd, |
46 } else { |
244 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
47 cmdName = fmt.Sprintf("%s;%s", previousCommandName, cmd.Name()) |
245 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) |
48 } |
246 } |
49 |
247 |
50 fmt.Fprintf(out, "\n '%s' {", cmdName) |
248 func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error { |
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) |
|
66 } |
|
67 |
|
68 fmt.Fprint(out, "\n break\n }") |
|
69 |
|
70 for _, subCmd := range cmd.Commands() { |
|
71 generatePowerShellSubcommandCases(out, subCmd, cmdName) |
|
72 } |
|
73 } |
|
74 |
|
75 func escapeStringForPowerShell(s string) string { |
|
76 return strings.Replace(s, "'", "''", -1) |
|
77 } |
|
78 |
|
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) |
249 buf := new(bytes.Buffer) |
82 |
250 genPowerShellComp(buf, c.Name(), includeDesc) |
83 var subCommandCases bytes.Buffer |
|
84 generatePowerShellSubcommandCases(&subCommandCases, c, "") |
|
85 fmt.Fprintf(buf, powerShellCompletionTemplate, c.Name(), c.Name(), subCommandCases.String()) |
|
86 |
|
87 _, err := buf.WriteTo(w) |
251 _, err := buf.WriteTo(w) |
88 return err |
252 return err |
89 } |
253 } |
90 |
254 |
91 // GenPowerShellCompletionFile generates PowerShell completion file. |
255 func (c *Command) genPowerShellCompletionFile(filename string, includeDesc bool) error { |
92 func (c *Command) GenPowerShellCompletionFile(filename string) error { |
|
93 outFile, err := os.Create(filename) |
256 outFile, err := os.Create(filename) |
94 if err != nil { |
257 if err != nil { |
95 return err |
258 return err |
96 } |
259 } |
97 defer outFile.Close() |
260 defer outFile.Close() |
98 |
261 |
99 return c.GenPowerShellCompletion(outFile) |
262 return c.genPowerShellCompletion(outFile, includeDesc) |
100 } |
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) |
|
285 } |