powershell completion with custom comp (#1208)
The current powershell completion is not very capable. Let's port it to the go custom completion logic to have a unified experience accross all shells. Powershell supports three different completion modes - TabCompleteNext (default windows style - on each key press the next option is displayed) - Complete (works like bash) - MenuComplete (works like zsh) You set the mode with `Set-PSReadLineKeyHandler -Key Tab -Function <mode>` To keep it backwards compatible `GenPowerShellCompletion` will not display descriptions. Use `GenPowerShellCompletionWithDesc` instead. Descriptions will only be displayed with `MenuComplete` or `Complete`. Signed-off-by: Paul Holzinger <paul.holzinger@web.de>
This commit is contained in:
		| @ -1,6 +1,3 @@ | |||||||
| // PowerShell completions are based on the amazing work from clap: |  | ||||||
| // https://github.com/clap-rs/clap/blob/3294d18efe5f264d12c9035f404c7d189d4824e1/src/completions/powershell.rs |  | ||||||
| // |  | ||||||
| // The generated scripts require PowerShell v5.0+ (which comes Windows 10, but | // The generated scripts require PowerShell v5.0+ (which comes Windows 10, but | ||||||
| // can be downloaded separately for windows 7 or 8.1). | // can be downloaded separately for windows 7 or 8.1). | ||||||
|  |  | ||||||
| @ -11,90 +8,278 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/spf13/pflag" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var powerShellCompletionTemplate = `using namespace System.Management.Automation | func genPowerShellComp(buf *bytes.Buffer, name string, includeDesc bool) { | ||||||
| using namespace System.Management.Automation.Language | 	compCmd := ShellCompRequestCmd | ||||||
| Register-ArgumentCompleter -Native -CommandName '%s' -ScriptBlock { | 	if !includeDesc { | ||||||
|     param($wordToComplete, $commandAst, $cursorPosition) | 		compCmd = ShellCompNoDescRequestCmd | ||||||
|     $commandElements = $commandAst.CommandElements |  | ||||||
|     $command = @( |  | ||||||
|         '%s' |  | ||||||
|         for ($i = 1; $i -lt $commandElements.Count; $i++) { |  | ||||||
|             $element = $commandElements[$i] |  | ||||||
|             if ($element -isnot [StringConstantExpressionAst] -or |  | ||||||
|                 $element.StringConstantType -ne [StringConstantType]::BareWord -or |  | ||||||
|                 $element.Value.StartsWith('-')) { |  | ||||||
|                 break |  | ||||||
| 	} | 	} | ||||||
|             $element.Value | 	buf.WriteString(fmt.Sprintf(`# powershell completion for %-36[1]s -*- shell-script -*- | ||||||
|         } |  | ||||||
|     ) -join ';' |  | ||||||
|     $completions = @(switch ($command) {%s |  | ||||||
|     }) |  | ||||||
|     $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | |  | ||||||
|         Sort-Object -Property ListItemText |  | ||||||
| }` |  | ||||||
|  |  | ||||||
| func generatePowerShellSubcommandCases(out io.Writer, cmd *Command, previousCommandName string) { | function __%[1]s_debug { | ||||||
| 	var cmdName string |     if ($env:BASH_COMP_DEBUG_FILE) { | ||||||
| 	if previousCommandName == "" { |         "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" | ||||||
| 		cmdName = cmd.Name() |     } | ||||||
| 	} else { | } | ||||||
| 		cmdName = fmt.Sprintf("%s;%s", previousCommandName, cmd.Name()) |  | ||||||
|  | filter __%[1]s_escapeStringWithSpecialChars { | ||||||
|  | `+"    $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|\"|`|\\||<|>|&','`$&'"+` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock { | ||||||
|  |     param( | ||||||
|  |             $WordToComplete, | ||||||
|  |             $CommandAst, | ||||||
|  |             $CursorPosition | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     # Get the current command line and convert into a string | ||||||
|  |     $Command = $CommandAst.CommandElements | ||||||
|  |     $Command = "$Command" | ||||||
|  |  | ||||||
|  |     __%[1]s_debug "" | ||||||
|  |     __%[1]s_debug "========= starting completion logic ==========" | ||||||
|  |     __%[1]s_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition" | ||||||
|  |  | ||||||
|  |     # The user could have moved the cursor backwards on the command-line. | ||||||
|  |     # We need to trigger completion from the $CursorPosition location, so we need | ||||||
|  |     # to truncate the command-line ($Command) up to the $CursorPosition location. | ||||||
|  |     # Make sure the $Command is longer then the $CursorPosition before we truncate. | ||||||
|  |     # This happens because the $Command does not include the last space.     | ||||||
|  |     if ($Command.Length -gt $CursorPosition) { | ||||||
|  |         $Command=$Command.Substring(0,$CursorPosition) | ||||||
|  |     } | ||||||
|  | 	__%[1]s_debug "Truncated command: $Command" | ||||||
|  | 	 | ||||||
|  |     $ShellCompDirectiveError=%[3]d | ||||||
|  |     $ShellCompDirectiveNoSpace=%[4]d | ||||||
|  |     $ShellCompDirectiveNoFileComp=%[5]d | ||||||
|  |     $ShellCompDirectiveFilterFileExt=%[6]d | ||||||
|  |     $ShellCompDirectiveFilterDirs=%[7]d | ||||||
|  |  | ||||||
|  | 	# Prepare the command to request completions for the program. | ||||||
|  |     # Split the command at the first space to separate the program and arguments. | ||||||
|  |     $Program,$Arguments = $Command.Split(" ",2) | ||||||
|  |     $RequestComp="$Program %[2]s $Arguments" | ||||||
|  |     __%[1]s_debug "RequestComp: $RequestComp" | ||||||
|  |  | ||||||
|  |     # we cannot use $WordToComplete because it  | ||||||
|  |     # has the wrong values if the cursor was moved | ||||||
|  |     # so use the last argument | ||||||
|  |     if ($WordToComplete -ne "" ) { | ||||||
|  |         $WordToComplete = $Arguments.Split(" ")[-1] | ||||||
|  |     } | ||||||
|  |     __%[1]s_debug "New WordToComplete: $WordToComplete" | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # Check for flag with equal sign | ||||||
|  |     $IsEqualFlag = ($WordToComplete -Like "--*=*" ) | ||||||
|  |     if ( $IsEqualFlag ) { | ||||||
|  |         __%[1]s_debug "Completing equal sign flag" | ||||||
|  |         # Remove the flag part | ||||||
|  |         $Flag,$WordToComplete = $WordToComplete.Split("=",2) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| 	fmt.Fprintf(out, "\n        '%s' {", cmdName) |     if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) { | ||||||
|  |         # If the last parameter is complete (there is a space following it) | ||||||
|  |         # We add an extra empty parameter so we can indicate this to the go method. | ||||||
|  |         __%[1]s_debug "Adding extra empty parameter" | ||||||
|  | `+"        # We need to use `\"`\" to pass an empty argument a \"\" or '' does not work!!!"+` | ||||||
|  | `+"        $RequestComp=\"$RequestComp\" + ' `\"`\"' "+` | ||||||
|  |     } | ||||||
|  |  | ||||||
| 	cmd.Flags().VisitAll(func(flag *pflag.Flag) { |     __%[1]s_debug "Calling $RequestComp" | ||||||
| 		if nonCompletableFlag(flag) { |     #call the command store the output in $out and redirect stderr and stdout to null | ||||||
|  |     # $Out is an array contains each line per element | ||||||
|  |     Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # get directive from last line | ||||||
|  |     [int]$Directive = $Out[-1].TrimStart(':') | ||||||
|  |     if ($Directive -eq "") { | ||||||
|  |         # There is no directive specified | ||||||
|  |         $Directive = 0 | ||||||
|  |     } | ||||||
|  |     __%[1]s_debug "The completion directive is: $Directive" | ||||||
|  |      | ||||||
|  |     # remove directive (last element) from out | ||||||
|  |     $Out = $Out | Where-Object { $_ -ne $Out[-1] } | ||||||
|  |     __%[1]s_debug "The completions are: $Out" | ||||||
|  |  | ||||||
|  |     if (($Directive -band $ShellCompDirectiveError) -ne 0 ) { | ||||||
|  |         # Error code.  No completion. | ||||||
|  |         __%[1]s_debug "Received error from custom completion go code" | ||||||
|         return |         return | ||||||
|     } |     } | ||||||
| 		usage := escapeStringForPowerShell(flag.Usage) |  | ||||||
| 		if len(flag.Shorthand) > 0 { |  | ||||||
| 			fmt.Fprintf(out, "\n            [CompletionResult]::new('-%s', '%s', [CompletionResultType]::ParameterName, '%s')", flag.Shorthand, flag.Shorthand, usage) |  | ||||||
| 		} |  | ||||||
| 		fmt.Fprintf(out, "\n            [CompletionResult]::new('--%s', '%s', [CompletionResultType]::ParameterName, '%s')", flag.Name, flag.Name, usage) |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	for _, subCmd := range cmd.Commands() { |     $Longest = 0 | ||||||
| 		usage := escapeStringForPowerShell(subCmd.Short) |     $Values = $Out | ForEach-Object { | ||||||
| 		fmt.Fprintf(out, "\n            [CompletionResult]::new('%s', '%s', [CompletionResultType]::ParameterValue, '%s')", subCmd.Name(), subCmd.Name(), usage) |         #Split the output in name and description | ||||||
|  | `+"        $Name, $Description = $_.Split(\"`t\",2)"+` | ||||||
|  |         __%[1]s_debug "Name: $Name Description: $Description" | ||||||
|  |  | ||||||
|  |         # Look for the longest completion so that we can format things nicely | ||||||
|  |         if ($Longest -lt $Name.Length) { | ||||||
|  |             $Longest = $Name.Length | ||||||
|         } |         } | ||||||
|  |  | ||||||
| 	fmt.Fprint(out, "\n            break\n        }") |         # Set the description to a one space string if there is none set. | ||||||
|  |         # This is needed because the CompletionResult does not accept an empty string as argument | ||||||
|  |         if (-Not $Description) { | ||||||
|  |             $Description = " " | ||||||
|  |         } | ||||||
|  |         @{Name="$Name";Description="$Description"} | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     $Space = " " | ||||||
|  |     if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) { | ||||||
|  |         # remove the space here | ||||||
|  |         __%[1]s_debug "ShellCompDirectiveNoSpace is called" | ||||||
|  |         $Space = "" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) { | ||||||
|  |         __%[1]s_debug "ShellCompDirectiveNoFileComp is called" | ||||||
|  |          | ||||||
|  |         if ($Values.Length -eq 0) { | ||||||
|  |             # Just print an empty string here so the  | ||||||
|  |             # shell does not start to complete paths. | ||||||
|  |             # We cannot use CompletionResult here because  | ||||||
|  |             # it does not accept an empty string as argument. | ||||||
|  |             "" | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or  | ||||||
|  |        (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 ))  { | ||||||
|  |         __%[1]s_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported" | ||||||
|  |  | ||||||
|  |         # return here to prevent the completion of the extensions | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $Values = $Values | Where-Object { | ||||||
|  |         # filter the result | ||||||
|  |         $_.Name -like "$WordToComplete*" | ||||||
|  |  | ||||||
|  |         # Join the flag back if we have a equal sign flag | ||||||
|  |         if ( $IsEqualFlag ) { | ||||||
|  |             __%[1]s_debug "Join the equal sign flag back to the completion value" | ||||||
|  |             $_.Name = $Flag + "=" + $_.Name | ||||||
|  |         } | ||||||
|  |     }  | ||||||
|  |  | ||||||
|  |     # Get the current mode | ||||||
|  |     $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function | ||||||
|  |     __%[1]s_debug "Mode: $Mode" | ||||||
|  |  | ||||||
|  |     $Values | ForEach-Object { | ||||||
|  |  | ||||||
|  |         # store temporay because switch will overwrite $_ | ||||||
|  |         $comp = $_ | ||||||
|  |  | ||||||
|  |         # Powershell supports three different completion modes | ||||||
|  |         # - TabCompleteNext (default windows style - on each key press the next option is displayed) | ||||||
|  |         # - Complete (works like bash) | ||||||
|  |         # - MenuComplete (works like zsh) | ||||||
|  |         # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function <mode> | ||||||
|  |  | ||||||
|  |         # CompletionResult Arguments: | ||||||
|  |         # 1) CompletionText text to be used as the auto completion result | ||||||
|  |         # 2) ListItemText   text to be displayed in the suggestion list | ||||||
|  |         # 3) ResultType     type of completion result | ||||||
|  |         # 4) ToolTip        text for the tooltip with details about the object | ||||||
|  |  | ||||||
|  |         switch ($Mode) { | ||||||
|  |  | ||||||
|  |             # bash like | ||||||
|  |             "Complete" { | ||||||
|  |  | ||||||
|  |                 if ($Values.Length -eq 1) { | ||||||
|  |                     __%[1]s_debug "Only one completion left" | ||||||
|  |  | ||||||
|  |                     # insert space after value | ||||||
|  |                     [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") | ||||||
|  |  | ||||||
|  |                 } else { | ||||||
|  |                     # Add the proper number of spaces to align the descriptions | ||||||
|  |                     while($comp.Name.Length -lt $Longest) { | ||||||
|  |                         $comp.Name = $comp.Name + " " | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     # Check for empty description and only add parentheses if needed | ||||||
|  |                     if ($($comp.Description) -eq " " ) { | ||||||
|  |                         $Description = "" | ||||||
|  |                     } else { | ||||||
|  |                         $Description = "  ($($comp.Description))" | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") | ||||||
|  |                 } | ||||||
|  |              } | ||||||
|  |  | ||||||
|  |             # zsh like | ||||||
|  |             "MenuComplete" { | ||||||
|  |                 # insert space after value | ||||||
|  |                 # MenuComplete will automatically show the ToolTip of  | ||||||
|  |                 # the highlighted value at the bottom of the suggestions. | ||||||
|  |                 [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             # TabCompleteNext and in case we get something unknown | ||||||
|  |             Default { | ||||||
|  |                 # Like MenuComplete but we don't want to add a space here because  | ||||||
|  |                 # the user need to press space anyway to get the completion. | ||||||
|  |                 # Description will not be shown because thats not possible with TabCompleteNext | ||||||
|  |                 [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
| 	for _, subCmd := range cmd.Commands() { |  | ||||||
| 		generatePowerShellSubcommandCases(out, subCmd, cmdName) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | `, name, compCmd, | ||||||
| func escapeStringForPowerShell(s string) string { | 		ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, | ||||||
| 	return strings.Replace(s, "'", "''", -1) | 		ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) | ||||||
| } | } | ||||||
|  |  | ||||||
| // GenPowerShellCompletion generates PowerShell completion file and writes to the passed writer. | func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error { | ||||||
| func (c *Command) GenPowerShellCompletion(w io.Writer) error { |  | ||||||
| 	buf := new(bytes.Buffer) | 	buf := new(bytes.Buffer) | ||||||
|  | 	genPowerShellComp(buf, c.Name(), includeDesc) | ||||||
| 	var subCommandCases bytes.Buffer |  | ||||||
| 	generatePowerShellSubcommandCases(&subCommandCases, c, "") |  | ||||||
| 	fmt.Fprintf(buf, powerShellCompletionTemplate, c.Name(), c.Name(), subCommandCases.String()) |  | ||||||
|  |  | ||||||
| 	_, err := buf.WriteTo(w) | 	_, err := buf.WriteTo(w) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
| // GenPowerShellCompletionFile generates PowerShell completion file. | func (c *Command) genPowerShellCompletionFile(filename string, includeDesc bool) error { | ||||||
| func (c *Command) GenPowerShellCompletionFile(filename string) error { |  | ||||||
| 	outFile, err := os.Create(filename) | 	outFile, err := os.Create(filename) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	defer outFile.Close() | 	defer outFile.Close() | ||||||
|  |  | ||||||
| 	return c.GenPowerShellCompletion(outFile) | 	return c.genPowerShellCompletion(outFile, includeDesc) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GenPowerShellCompletionFile generates powershell completion file without descriptions. | ||||||
|  | func (c *Command) GenPowerShellCompletionFile(filename string) error { | ||||||
|  | 	return c.genPowerShellCompletionFile(filename, false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GenPowerShellCompletion generates powershell completion file without descriptions | ||||||
|  | // and writes it to the passed writer. | ||||||
|  | func (c *Command) GenPowerShellCompletion(w io.Writer) error { | ||||||
|  | 	return c.genPowerShellCompletion(w, false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GenPowerShellCompletionFileWithDesc generates powershell completion file with descriptions. | ||||||
|  | func (c *Command) GenPowerShellCompletionFileWithDesc(filename string) error { | ||||||
|  | 	return c.genPowerShellCompletionFile(filename, true) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GenPowerShellCompletionWithDesc generates powershell completion file with descriptions | ||||||
|  | // and writes it to the passed writer. | ||||||
|  | func (c *Command) GenPowerShellCompletionWithDesc(w io.Writer) error { | ||||||
|  | 	return c.genPowerShellCompletion(w, true) | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,16 +1,3 @@ | |||||||
| # Generating PowerShell Completions For Your Own cobra.Command | # Generating PowerShell Completions For Your Own cobra.Command | ||||||
|  |  | ||||||
| Cobra can generate PowerShell completion scripts. Users need PowerShell version 5.0 or above, which comes with Windows 10 and can be downloaded separately for Windows 7 or 8.1. They can then write the completions to a file and source this file from their PowerShell profile, which is referenced by the `$Profile` environment variable. See `Get-Help about_Profiles` for more info about PowerShell profiles. | Please refer to [Shell Completions](shell_completions.md#powershell-completions) for details. | ||||||
|  |  | ||||||
| *Note*: PowerShell completions have not (yet?) been aligned to Cobra's generic shell completion support.  This implies the PowerShell completions are not as rich as for other shells (see [What's not yet supported](#whats-not-yet-supported)), and may behave slightly differently.  They are still very useful for PowerShell users. |  | ||||||
|  |  | ||||||
| # What's supported |  | ||||||
|  |  | ||||||
| - Completion for subcommands using their `.Short` description |  | ||||||
| - Completion for non-hidden flags using their `.Name` and `.Shorthand` |  | ||||||
|  |  | ||||||
| # What's not yet supported |  | ||||||
|  |  | ||||||
| - Command aliases |  | ||||||
| - Required, filename or custom flags (they will work like normal flags) |  | ||||||
| - Custom completion scripts |  | ||||||
|  | |||||||
| @ -1,122 +0,0 @@ | |||||||
| package cobra |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestPowerShellCompletion(t *testing.T) { |  | ||||||
| 	tcs := []struct { |  | ||||||
| 		name                string |  | ||||||
| 		root                *Command |  | ||||||
| 		expectedExpressions []string |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name: "trivial", |  | ||||||
| 			root: &Command{Use: "trivialapp"}, |  | ||||||
| 			expectedExpressions: []string{ |  | ||||||
| 				"Register-ArgumentCompleter -Native -CommandName 'trivialapp' -ScriptBlock", |  | ||||||
| 				"$command = @(\n        'trivialapp'\n", |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "tree", |  | ||||||
| 			root: func() *Command { |  | ||||||
| 				r := &Command{Use: "tree"} |  | ||||||
|  |  | ||||||
| 				sub1 := &Command{Use: "sub1"} |  | ||||||
| 				r.AddCommand(sub1) |  | ||||||
|  |  | ||||||
| 				sub11 := &Command{Use: "sub11"} |  | ||||||
| 				sub12 := &Command{Use: "sub12"} |  | ||||||
|  |  | ||||||
| 				sub1.AddCommand(sub11) |  | ||||||
| 				sub1.AddCommand(sub12) |  | ||||||
|  |  | ||||||
| 				sub2 := &Command{Use: "sub2"} |  | ||||||
| 				r.AddCommand(sub2) |  | ||||||
|  |  | ||||||
| 				sub21 := &Command{Use: "sub21"} |  | ||||||
| 				sub22 := &Command{Use: "sub22"} |  | ||||||
|  |  | ||||||
| 				sub2.AddCommand(sub21) |  | ||||||
| 				sub2.AddCommand(sub22) |  | ||||||
|  |  | ||||||
| 				return r |  | ||||||
| 			}(), |  | ||||||
| 			expectedExpressions: []string{ |  | ||||||
| 				"'tree'", |  | ||||||
| 				"[CompletionResult]::new('sub1', 'sub1', [CompletionResultType]::ParameterValue, '')", |  | ||||||
| 				"[CompletionResult]::new('sub2', 'sub2', [CompletionResultType]::ParameterValue, '')", |  | ||||||
| 				"'tree;sub1'", |  | ||||||
| 				"[CompletionResult]::new('sub11', 'sub11', [CompletionResultType]::ParameterValue, '')", |  | ||||||
| 				"[CompletionResult]::new('sub12', 'sub12', [CompletionResultType]::ParameterValue, '')", |  | ||||||
| 				"'tree;sub1;sub11'", |  | ||||||
| 				"'tree;sub1;sub12'", |  | ||||||
| 				"'tree;sub2'", |  | ||||||
| 				"[CompletionResult]::new('sub21', 'sub21', [CompletionResultType]::ParameterValue, '')", |  | ||||||
| 				"[CompletionResult]::new('sub22', 'sub22', [CompletionResultType]::ParameterValue, '')", |  | ||||||
| 				"'tree;sub2;sub21'", |  | ||||||
| 				"'tree;sub2;sub22'", |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "flags", |  | ||||||
| 			root: func() *Command { |  | ||||||
| 				r := &Command{Use: "flags"} |  | ||||||
| 				r.Flags().StringP("flag1", "a", "", "") |  | ||||||
| 				r.Flags().String("flag2", "", "") |  | ||||||
|  |  | ||||||
| 				sub1 := &Command{Use: "sub1"} |  | ||||||
| 				sub1.Flags().StringP("flag3", "c", "", "") |  | ||||||
| 				r.AddCommand(sub1) |  | ||||||
|  |  | ||||||
| 				return r |  | ||||||
| 			}(), |  | ||||||
| 			expectedExpressions: []string{ |  | ||||||
| 				"'flags'", |  | ||||||
| 				"[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, '')", |  | ||||||
| 				"[CompletionResult]::new('--flag1', 'flag1', [CompletionResultType]::ParameterName, '')", |  | ||||||
| 				"[CompletionResult]::new('--flag2', 'flag2', [CompletionResultType]::ParameterName, '')", |  | ||||||
| 				"[CompletionResult]::new('sub1', 'sub1', [CompletionResultType]::ParameterValue, '')", |  | ||||||
| 				"'flags;sub1'", |  | ||||||
| 				"[CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, '')", |  | ||||||
| 				"[CompletionResult]::new('--flag3', 'flag3', [CompletionResultType]::ParameterName, '')", |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "usage", |  | ||||||
| 			root: func() *Command { |  | ||||||
| 				r := &Command{Use: "usage"} |  | ||||||
| 				r.Flags().String("flag", "", "this describes the usage of the 'flag' flag") |  | ||||||
|  |  | ||||||
| 				sub1 := &Command{ |  | ||||||
| 					Use:   "sub1", |  | ||||||
| 					Short: "short describes 'sub1'", |  | ||||||
| 				} |  | ||||||
| 				r.AddCommand(sub1) |  | ||||||
|  |  | ||||||
| 				return r |  | ||||||
| 			}(), |  | ||||||
| 			expectedExpressions: []string{ |  | ||||||
| 				"[CompletionResult]::new('--flag', 'flag', [CompletionResultType]::ParameterName, 'this describes the usage of the ''flag'' flag')", |  | ||||||
| 				"[CompletionResult]::new('sub1', 'sub1', [CompletionResultType]::ParameterValue, 'short describes ''sub1''')", |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, tc := range tcs { |  | ||||||
| 		t.Run(tc.name, func(t *testing.T) { |  | ||||||
| 			buf := new(bytes.Buffer) |  | ||||||
| 			tc.root.GenPowerShellCompletion(buf) |  | ||||||
| 			output := buf.String() |  | ||||||
|  |  | ||||||
| 			for _, expectedExpression := range tc.expectedExpressions { |  | ||||||
| 				if !strings.Contains(output, expectedExpression) { |  | ||||||
| 					t.Errorf("Expected completion to contain %q somewhere; got %q", expectedExpression, output) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -49,6 +49,14 @@ $ yourprogram completion fish | source | |||||||
|  |  | ||||||
| # To load completions for each session, execute once: | # To load completions for each session, execute once: | ||||||
| $ yourprogram completion fish > ~/.config/fish/completions/yourprogram.fish | $ yourprogram completion fish > ~/.config/fish/completions/yourprogram.fish | ||||||
|  |  | ||||||
|  | Powershell: | ||||||
|  |  | ||||||
|  | PS> yourprogram completion powershell | Out-String | Invoke-Expression | ||||||
|  |  | ||||||
|  | # To load completions for every new session, run: | ||||||
|  | PS> yourprogram completion powershell > yourprogram.ps1 | ||||||
|  | # and source this file from your powershell profile. | ||||||
| `, | `, | ||||||
| 	DisableFlagsInUseLine: true, | 	DisableFlagsInUseLine: true, | ||||||
| 	ValidArgs:             []string{"bash", "zsh", "fish", "powershell"}, | 	ValidArgs:             []string{"bash", "zsh", "fish", "powershell"}, | ||||||
| @ -316,7 +324,7 @@ cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, | |||||||
| ``` | ``` | ||||||
| ### Descriptions for completions | ### Descriptions for completions | ||||||
|  |  | ||||||
| Both `zsh` and `fish` allow for descriptions to annotate completion choices.  For commands and flags, Cobra will provide the descriptions automatically, based on usage information.  For example, using zsh: | `zsh`, `fish` and `powershell` allow for descriptions to annotate completion choices.  For commands and flags, Cobra will provide the descriptions automatically, based on usage information.  For example, using zsh: | ||||||
| ``` | ``` | ||||||
| $ helm s[tab] | $ helm s[tab] | ||||||
| search  -- search for a keyword in charts | search  -- search for a keyword in charts | ||||||
| @ -390,7 +398,7 @@ search  show  status | |||||||
| ### Limitations | ### Limitations | ||||||
|  |  | ||||||
| * Custom completions implemented in Bash scripting (legacy) are not supported and will be ignored for `zsh` (including the use of the `BashCompCustom` flag annotation). | * Custom completions implemented in Bash scripting (legacy) are not supported and will be ignored for `zsh` (including the use of the `BashCompCustom` flag annotation). | ||||||
|   * You should instead use `ValidArgsFunction` and `RegisterFlagCompletionFunc()` which are portable to the different shells (`bash`, `zsh`, `fish`). |   * You should instead use `ValidArgsFunction` and `RegisterFlagCompletionFunc()` which are portable to the different shells (`bash`, `zsh`, `fish`, `powershell`). | ||||||
| * The function `MarkFlagCustom()` is not supported and will be ignored for `zsh`. | * The function `MarkFlagCustom()` is not supported and will be ignored for `zsh`. | ||||||
|   * You should instead use `RegisterFlagCompletionFunc()`. |   * You should instead use `RegisterFlagCompletionFunc()`. | ||||||
|  |  | ||||||
| @ -416,7 +424,7 @@ search  show  status | |||||||
| ### Limitations | ### Limitations | ||||||
|  |  | ||||||
| * Custom completions implemented in Bash scripting (legacy) are not supported and will be ignored for `fish` (including the use of the `BashCompCustom` flag annotation). | * Custom completions implemented in Bash scripting (legacy) are not supported and will be ignored for `fish` (including the use of the `BashCompCustom` flag annotation). | ||||||
|   * You should instead use `ValidArgsFunction` and `RegisterFlagCompletionFunc()` which are portable to the different shells (`bash`, `zsh`, `fish`). |   * You should instead use `ValidArgsFunction` and `RegisterFlagCompletionFunc()` which are portable to the different shells (`bash`, `zsh`, `fish`, `powershell`). | ||||||
| * The function `MarkFlagCustom()` is not supported and will be ignored for `fish`. | * The function `MarkFlagCustom()` is not supported and will be ignored for `fish`. | ||||||
|   * You should instead use `RegisterFlagCompletionFunc()`. |   * You should instead use `RegisterFlagCompletionFunc()`. | ||||||
| * The following flag completion annotations are not supported and will be ignored for `fish`: | * The following flag completion annotations are not supported and will be ignored for `fish`: | ||||||
| @ -431,4 +439,46 @@ search  show  status | |||||||
|  |  | ||||||
| ## PowerShell completions | ## PowerShell completions | ||||||
|  |  | ||||||
| Please refer to [PowerShell Completions](powershell_completions.md) for details. | Cobra supports native PowerShell completions generated from the root `cobra.Command`. You can use the `command.GenPowerShellCompletion()` or `command.GenPowerShellCompletionFile()` functions. To include descriptions use `command.GenPowerShellCompletionWithDesc()` and `command.GenPowerShellCompletionFileWithDesc()`. Cobra will provide the description automatically based on usage information. You can choose to make this option configurable by your users. | ||||||
|  |  | ||||||
|  | The script is designed to support all three Powershell completion modes: | ||||||
|  |  | ||||||
|  | * TabCompleteNext (default windows style - on each key press the next option is displayed) | ||||||
|  | * Complete (works like bash) | ||||||
|  | * MenuComplete (works like zsh) | ||||||
|  |  | ||||||
|  | You set the mode with `Set-PSReadLineKeyHandler -Key Tab -Function <mode>`. Descriptions are only displayed when using the `Complete` or `MenuComplete` mode. | ||||||
|  |  | ||||||
|  | Users need PowerShell version 5.0 or above, which comes with Windows 10 and can be downloaded separately for Windows 7 or 8.1. They can then write the completions to a file and source this file from their PowerShell profile, which is referenced by the `$Profile` environment variable. See `Get-Help about_Profiles` for more info about PowerShell profiles. | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | # With descriptions and Mode 'Complete' | ||||||
|  | $ helm s[tab] | ||||||
|  | search  (search for a keyword in charts)  show  (show information of a chart)  status  (displays the status of the named release) | ||||||
|  |  | ||||||
|  | # With descriptions and Mode 'MenuComplete' The description of the current selected value will be displayed below the suggestions. | ||||||
|  | $ helm s[tab] | ||||||
|  | search    show     status   | ||||||
|  |  | ||||||
|  | search for a keyword in charts | ||||||
|  |  | ||||||
|  | # Without descriptions | ||||||
|  | $ helm s[tab] | ||||||
|  | search  show  status | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Limitations | ||||||
|  |  | ||||||
|  | * Custom completions implemented in Bash scripting (legacy) are not supported and will be ignored for `powershell` (including the use of the `BashCompCustom` flag annotation). | ||||||
|  |   * You should instead use `ValidArgsFunction` and `RegisterFlagCompletionFunc()` which are portable to the different shells (`bash`, `zsh`, `fish`, `powershell`). | ||||||
|  | * The function `MarkFlagCustom()` is not supported and will be ignored for `powershell`. | ||||||
|  |   * You should instead use `RegisterFlagCompletionFunc()`. | ||||||
|  | * The following flag completion annotations are not supported and will be ignored for `powershell`: | ||||||
|  |   * `BashCompFilenameExt` (filtering by file extension) | ||||||
|  |   * `BashCompSubdirsInDir` (filtering by directory) | ||||||
|  | * The functions corresponding to the above annotations are consequently not supported and will be ignored for `powershell`: | ||||||
|  |   * `MarkFlagFilename()` and `MarkPersistentFlagFilename()` (filtering by file extension) | ||||||
|  |   * `MarkFlagDirname()` and `MarkPersistentFlagDirname()` (filtering by directory) | ||||||
|  | * Similarly, the following completion directives are not supported and will be ignored for `powershell`: | ||||||
|  |   * `ShellCompDirectiveFilterFileExt` (filtering by file extension) | ||||||
|  |   * `ShellCompDirectiveFilterDirs` (filtering by directory) | ||||||
		Reference in New Issue
	
	Block a user