...

Source file src/github.com/spf13/cobra/powershell_completions.go

Documentation: github.com/spf13/cobra

     1  // Copyright 2013-2023 The Cobra Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // The generated scripts require PowerShell v5.0+ (which comes Windows 10, but
    16  // can be downloaded separately for windows 7 or 8.1).
    17  
    18  package cobra
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"strings"
    26  )
    27  
    28  func genPowerShellComp(buf io.StringWriter, name string, includeDesc bool) {
    29  	// Variables should not contain a '-' or ':' character
    30  	nameForVar := name
    31  	nameForVar = strings.Replace(nameForVar, "-", "_", -1)
    32  	nameForVar = strings.Replace(nameForVar, ":", "_", -1)
    33  
    34  	compCmd := ShellCompRequestCmd
    35  	if !includeDesc {
    36  		compCmd = ShellCompNoDescRequestCmd
    37  	}
    38  	WriteStringAndCheck(buf, fmt.Sprintf(`# powershell completion for %-36[1]s -*- shell-script -*-
    39  
    40  function __%[1]s_debug {
    41      if ($env:BASH_COMP_DEBUG_FILE) {
    42          "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE"
    43      }
    44  }
    45  
    46  filter __%[1]s_escapeStringWithSpecialChars {
    47  `+"    $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|\"|`|\\||<|>|&','`$&'"+`
    48  }
    49  
    50  [scriptblock]${__%[2]sCompleterBlock} = {
    51      param(
    52              $WordToComplete,
    53              $CommandAst,
    54              $CursorPosition
    55          )
    56  
    57      # Get the current command line and convert into a string
    58      $Command = $CommandAst.CommandElements
    59      $Command = "$Command"
    60  
    61      __%[1]s_debug ""
    62      __%[1]s_debug "========= starting completion logic =========="
    63      __%[1]s_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition"
    64  
    65      # The user could have moved the cursor backwards on the command-line.
    66      # We need to trigger completion from the $CursorPosition location, so we need
    67      # to truncate the command-line ($Command) up to the $CursorPosition location.
    68      # Make sure the $Command is longer then the $CursorPosition before we truncate.
    69      # This happens because the $Command does not include the last space.
    70      if ($Command.Length -gt $CursorPosition) {
    71          $Command=$Command.Substring(0,$CursorPosition)
    72      }
    73      __%[1]s_debug "Truncated command: $Command"
    74  
    75      $ShellCompDirectiveError=%[4]d
    76      $ShellCompDirectiveNoSpace=%[5]d
    77      $ShellCompDirectiveNoFileComp=%[6]d
    78      $ShellCompDirectiveFilterFileExt=%[7]d
    79      $ShellCompDirectiveFilterDirs=%[8]d
    80      $ShellCompDirectiveKeepOrder=%[9]d
    81  
    82      # Prepare the command to request completions for the program.
    83      # Split the command at the first space to separate the program and arguments.
    84      $Program,$Arguments = $Command.Split(" ",2)
    85  
    86      $RequestComp="$Program %[3]s $Arguments"
    87      __%[1]s_debug "RequestComp: $RequestComp"
    88  
    89      # we cannot use $WordToComplete because it
    90      # has the wrong values if the cursor was moved
    91      # so use the last argument
    92      if ($WordToComplete -ne "" ) {
    93          $WordToComplete = $Arguments.Split(" ")[-1]
    94      }
    95      __%[1]s_debug "New WordToComplete: $WordToComplete"
    96  
    97  
    98      # Check for flag with equal sign
    99      $IsEqualFlag = ($WordToComplete -Like "--*=*" )
   100      if ( $IsEqualFlag ) {
   101          __%[1]s_debug "Completing equal sign flag"
   102          # Remove the flag part
   103          $Flag,$WordToComplete = $WordToComplete.Split("=",2)
   104      }
   105  
   106      if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) {
   107          # If the last parameter is complete (there is a space following it)
   108          # We add an extra empty parameter so we can indicate this to the go method.
   109          __%[1]s_debug "Adding extra empty parameter"
   110          # PowerShell 7.2+ changed the way how the arguments are passed to executables,
   111          # so for pre-7.2 or when Legacy argument passing is enabled we need to use
   112  `+"        # `\"`\" to pass an empty argument, a \"\" or '' does not work!!!"+`
   113          if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or
   114              ($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or
   115              (($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and
   116                $PSNativeCommandArgumentPassing -eq 'Legacy')) {
   117  `+"             $RequestComp=\"$RequestComp\" + ' `\"`\"'"+`
   118          } else {
   119               $RequestComp="$RequestComp" + ' ""'
   120          }
   121      }
   122  
   123      __%[1]s_debug "Calling $RequestComp"
   124      # First disable ActiveHelp which is not supported for Powershell
   125      ${env:%[10]s}=0
   126  
   127      #call the command store the output in $out and redirect stderr and stdout to null
   128      # $Out is an array contains each line per element
   129      Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null
   130  
   131      # get directive from last line
   132      [int]$Directive = $Out[-1].TrimStart(':')
   133      if ($Directive -eq "") {
   134          # There is no directive specified
   135          $Directive = 0
   136      }
   137      __%[1]s_debug "The completion directive is: $Directive"
   138  
   139      # remove directive (last element) from out
   140      $Out = $Out | Where-Object { $_ -ne $Out[-1] }
   141      __%[1]s_debug "The completions are: $Out"
   142  
   143      if (($Directive -band $ShellCompDirectiveError) -ne 0 ) {
   144          # Error code.  No completion.
   145          __%[1]s_debug "Received error from custom completion go code"
   146          return
   147      }
   148  
   149      $Longest = 0
   150      [Array]$Values = $Out | ForEach-Object {
   151          #Split the output in name and description
   152  `+"        $Name, $Description = $_.Split(\"`t\",2)"+`
   153          __%[1]s_debug "Name: $Name Description: $Description"
   154  
   155          # Look for the longest completion so that we can format things nicely
   156          if ($Longest -lt $Name.Length) {
   157              $Longest = $Name.Length
   158          }
   159  
   160          # Set the description to a one space string if there is none set.
   161          # This is needed because the CompletionResult does not accept an empty string as argument
   162          if (-Not $Description) {
   163              $Description = " "
   164          }
   165          @{Name="$Name";Description="$Description"}
   166      }
   167  
   168  
   169      $Space = " "
   170      if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) {
   171          # remove the space here
   172          __%[1]s_debug "ShellCompDirectiveNoSpace is called"
   173          $Space = ""
   174      }
   175  
   176      if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or
   177         (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 ))  {
   178          __%[1]s_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported"
   179  
   180          # return here to prevent the completion of the extensions
   181          return
   182      }
   183  
   184      $Values = $Values | Where-Object {
   185          # filter the result
   186          $_.Name -like "$WordToComplete*"
   187  
   188          # Join the flag back if we have an equal sign flag
   189          if ( $IsEqualFlag ) {
   190              __%[1]s_debug "Join the equal sign flag back to the completion value"
   191              $_.Name = $Flag + "=" + $_.Name
   192          }
   193      }
   194  
   195      # we sort the values in ascending order by name if keep order isn't passed
   196      if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) {
   197          $Values = $Values | Sort-Object -Property Name
   198      }
   199  
   200      if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) {
   201          __%[1]s_debug "ShellCompDirectiveNoFileComp is called"
   202  
   203          if ($Values.Length -eq 0) {
   204              # Just print an empty string here so the
   205              # shell does not start to complete paths.
   206              # We cannot use CompletionResult here because
   207              # it does not accept an empty string as argument.
   208              ""
   209              return
   210          }
   211      }
   212  
   213      # Get the current mode
   214      $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function
   215      __%[1]s_debug "Mode: $Mode"
   216  
   217      $Values | ForEach-Object {
   218  
   219          # store temporary because switch will overwrite $_
   220          $comp = $_
   221  
   222          # PowerShell supports three different completion modes
   223          # - TabCompleteNext (default windows style - on each key press the next option is displayed)
   224          # - Complete (works like bash)
   225          # - MenuComplete (works like zsh)
   226          # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function <mode>
   227  
   228          # CompletionResult Arguments:
   229          # 1) CompletionText text to be used as the auto completion result
   230          # 2) ListItemText   text to be displayed in the suggestion list
   231          # 3) ResultType     type of completion result
   232          # 4) ToolTip        text for the tooltip with details about the object
   233  
   234          switch ($Mode) {
   235  
   236              # bash like
   237              "Complete" {
   238  
   239                  if ($Values.Length -eq 1) {
   240                      __%[1]s_debug "Only one completion left"
   241  
   242                      # insert space after value
   243                      [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
   244  
   245                  } else {
   246                      # Add the proper number of spaces to align the descriptions
   247                      while($comp.Name.Length -lt $Longest) {
   248                          $comp.Name = $comp.Name + " "
   249                      }
   250  
   251                      # Check for empty description and only add parentheses if needed
   252                      if ($($comp.Description) -eq " " ) {
   253                          $Description = ""
   254                      } else {
   255                          $Description = "  ($($comp.Description))"
   256                      }
   257  
   258                      [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)")
   259                  }
   260               }
   261  
   262              # zsh like
   263              "MenuComplete" {
   264                  # insert space after value
   265                  # MenuComplete will automatically show the ToolTip of
   266                  # the highlighted value at the bottom of the suggestions.
   267                  [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
   268              }
   269  
   270              # TabCompleteNext and in case we get something unknown
   271              Default {
   272                  # Like MenuComplete but we don't want to add a space here because
   273                  # the user need to press space anyway to get the completion.
   274                  # Description will not be shown because that's not possible with TabCompleteNext
   275                  [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
   276              }
   277          }
   278  
   279      }
   280  }
   281  
   282  Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock ${__%[2]sCompleterBlock}
   283  `, name, nameForVar, compCmd,
   284  		ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
   285  		ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, activeHelpEnvVar(name)))
   286  }
   287  
   288  func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error {
   289  	buf := new(bytes.Buffer)
   290  	genPowerShellComp(buf, c.Name(), includeDesc)
   291  	_, err := buf.WriteTo(w)
   292  	return err
   293  }
   294  
   295  func (c *Command) genPowerShellCompletionFile(filename string, includeDesc bool) error {
   296  	outFile, err := os.Create(filename)
   297  	if err != nil {
   298  		return err
   299  	}
   300  	defer outFile.Close()
   301  
   302  	return c.genPowerShellCompletion(outFile, includeDesc)
   303  }
   304  
   305  // GenPowerShellCompletionFile generates powershell completion file without descriptions.
   306  func (c *Command) GenPowerShellCompletionFile(filename string) error {
   307  	return c.genPowerShellCompletionFile(filename, false)
   308  }
   309  
   310  // GenPowerShellCompletion generates powershell completion file without descriptions
   311  // and writes it to the passed writer.
   312  func (c *Command) GenPowerShellCompletion(w io.Writer) error {
   313  	return c.genPowerShellCompletion(w, false)
   314  }
   315  
   316  // GenPowerShellCompletionFileWithDesc generates powershell completion file with descriptions.
   317  func (c *Command) GenPowerShellCompletionFileWithDesc(filename string) error {
   318  	return c.genPowerShellCompletionFile(filename, true)
   319  }
   320  
   321  // GenPowerShellCompletionWithDesc generates powershell completion file with descriptions
   322  // and writes it to the passed writer.
   323  func (c *Command) GenPowerShellCompletionWithDesc(w io.Writer) error {
   324  	return c.genPowerShellCompletion(w, true)
   325  }
   326  

View as plain text