vendor/github.com/spf13/cobra/powershell_completions.go
changeset 256 6d9efbef00a9
parent 251 1c52a0eeb952
child 260 445e01aede7e
equal deleted inserted replaced
255:4f153a23adab 256:6d9efbef00a9
     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 }