// Copyright 2013-2023 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // The generated scripts require PowerShell v5.0+ (which comes Windows 10, but // can be downloaded separately for windows 7 or 8.1). package cobra import ( "bytes" "fmt" "io" "os" "strings" ) func genPowerShellComp(buf io.StringWriter, name string, includeDesc bool) { // Variables should not contain a '-' or ':' character nameForVar := name nameForVar = strings.ReplaceAll(nameForVar, "-", "_") nameForVar = strings.ReplaceAll(nameForVar, ":", "_") compCmd := ShellCompRequestCmd if !includeDesc { compCmd = ShellCompNoDescRequestCmd } WriteStringAndCheck(buf, fmt.Sprintf(`# powershell completion for %-36[1]s -*- shell-script -*- function __%[1]s_debug { if ($env:BASH_COMP_DEBUG_FILE) { "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" } } filter __%[1]s_escapeStringWithSpecialChars { `+" $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|\"|`|\\||<|>|&','`$&'"+` } [scriptblock]${__%[2]sCompleterBlock} = { 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=%[4]d $ShellCompDirectiveNoSpace=%[5]d $ShellCompDirectiveNoFileComp=%[6]d $ShellCompDirectiveFilterFileExt=%[7]d $ShellCompDirectiveFilterDirs=%[8]d $ShellCompDirectiveKeepOrder=%[9]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 %[3]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) } 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" # PowerShell 7.2+ changed the way how the arguments are passed to executables, # so for pre-7.2 or when Legacy argument passing is enabled we need to use `+" # `\"`\" to pass an empty argument, a \"\" or '' does not work!!!"+` if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or ($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or (($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and $PSNativeCommandArgumentPassing -eq 'Legacy')) { `+" $RequestComp=\"$RequestComp\" + ' `\"`\"'"+` } else { $RequestComp="$RequestComp" + ' ""' } } __%[1]s_debug "Calling $RequestComp" # First disable ActiveHelp which is not supported for Powershell ${env:%[10]s}=0 #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 } $Longest = 0 [Array]$Values = $Out | ForEach-Object { #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 } # 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 = " " } New-Object -TypeName PSCustomObject -Property @{ 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 $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 an equal sign flag if ( $IsEqualFlag ) { __%[1]s_debug "Join the equal sign flag back to the completion value" $_.Name = $Flag + "=" + $_.Name } } # we sort the values in ascending order by name if keep order isn't passed if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) { $Values = $Values | Sort-Object -Property Name } 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 } } # Get the current mode $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function __%[1]s_debug "Mode: $Mode" $Values | ForEach-Object { # store temporary 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 # 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 $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") } else { $CompletionText } } 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))" } $CompletionText = "$($comp.Name)$Description" if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") } else { $CompletionText } } } # zsh like "MenuComplete" { # insert space after value # MenuComplete will automatically show the ToolTip of # the highlighted value at the bottom of the suggestions. $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") } else { $CompletionText } } # 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 that's not possible with TabCompleteNext $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") } else { $CompletionText } } } } } Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock ${__%[2]sCompleterBlock} `, name, nameForVar, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, activeHelpEnvVar(name))) } func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error { buf := new(bytes.Buffer) genPowerShellComp(buf, c.Name(), includeDesc) _, err := buf.WriteTo(w) return err } func (c *Command) genPowerShellCompletionFile(filename string, includeDesc bool) error { outFile, err := os.Create(filename) if err != nil { return err } defer outFile.Close() 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) }