mirror of
https://github.com/spf13/cobra
synced 2024-11-16 18:57:08 +00:00
15823e76b9
In the bash shell we used to print ActiveHelp messages on every tab-press. In the example below, notice the "Command help" line which is ActiveHelp: bash-5.1$ tanzu context u[tab] Command help: Configure and manage contexts for the Tanzu CLI bash-5.1$ tanzu context u[tab] Command help: Configure and manage contexts for the Tanzu CLI bash-5.1$ tanzu context u unset (Unset the active context so that it is not used by default.) use (Set the context to be used by default) bash-5.1$ tanzu context u Above, on the first [tab] press, only the ActiveHelp is printed. On the second [tab] press, the ActiveHelp is printed again, followed by a re-print of the command-line, followed by the completions choices. The separation between ActiveHelp and completion choices makes the ActiveHelp harder to see. Furthermore, I find the double printing of the ActiveHelp string to look bad. Note that for zsh, the UX is different and that ActiveHelp messages are printed at the same time as the completion choices. This commit aligns the UX for ActiveHelp in bash with the one for zsh: if there are other completions to be shown, the ActiveHelp messages are printed at the same time. New behaviour: 1- ActiveHelp is no longer printed on the first [tab] press. This is better aligned with bash's standard approach. 2- ActiveHelp is printed on the second [tab] press, above the completion choices, with a `--` delimiter. 3- If there are no completion choices, the `--` delimiter is omitted. This behaviour is the same as what is done for zsh (except that for zsh the first [tab] press immediately shows completion choices). Below is the above example, but using this commit. Notice the more concise and easier to read completion output: bash-5.1$ tanzu context u[tab][tab] Command help: Configure and manage contexts for the Tanzu CLI -- unset (Unset the active context so that it is not used by default.) use (Set the context to be used by default) bash-5.1$ tanzu context u Signed-off-by: Marc Khouzam <marc.khouzam@gmail.com>
438 lines
15 KiB
Go
438 lines
15 KiB
Go
// 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.
|
|
|
|
package cobra
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error {
|
|
buf := new(bytes.Buffer)
|
|
genBashComp(buf, c.Name(), includeDesc)
|
|
_, err := buf.WriteTo(w)
|
|
return err
|
|
}
|
|
|
|
func genBashComp(buf io.StringWriter, name string, includeDesc bool) {
|
|
compCmd := ShellCompRequestCmd
|
|
if !includeDesc {
|
|
compCmd = ShellCompNoDescRequestCmd
|
|
}
|
|
|
|
WriteStringAndCheck(buf, fmt.Sprintf(`# bash completion V2 for %-36[1]s -*- shell-script -*-
|
|
|
|
__%[1]s_debug()
|
|
{
|
|
if [[ -n ${BASH_COMP_DEBUG_FILE-} ]]; then
|
|
echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
|
|
fi
|
|
}
|
|
|
|
# Macs have bash3 for which the bash-completion package doesn't include
|
|
# _init_completion. This is a minimal version of that function.
|
|
__%[1]s_init_completion()
|
|
{
|
|
COMPREPLY=()
|
|
_get_comp_words_by_ref "$@" cur prev words cword
|
|
}
|
|
|
|
# This function calls the %[1]s program to obtain the completion
|
|
# results and the directive. It fills the 'out' and 'directive' vars.
|
|
__%[1]s_get_completion_results() {
|
|
local requestComp lastParam lastChar args
|
|
|
|
# Prepare the command to request completions for the program.
|
|
# Calling ${words[0]} instead of directly %[1]s allows handling aliases
|
|
args=("${words[@]:1}")
|
|
requestComp="${words[0]} %[2]s ${args[*]}"
|
|
|
|
lastParam=${words[$((${#words[@]}-1))]}
|
|
lastChar=${lastParam:$((${#lastParam}-1)):1}
|
|
__%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}"
|
|
|
|
if [[ -z ${cur} && ${lastChar} != = ]]; then
|
|
# 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"
|
|
requestComp="${requestComp} ''"
|
|
fi
|
|
|
|
# When completing a flag with an = (e.g., %[1]s -n=<TAB>)
|
|
# bash focuses on the part after the =, so we need to remove
|
|
# the flag part from $cur
|
|
if [[ ${cur} == -*=* ]]; then
|
|
cur="${cur#*=}"
|
|
fi
|
|
|
|
__%[1]s_debug "Calling ${requestComp}"
|
|
# Use eval to handle any environment variables and such
|
|
out=$(eval "${requestComp}" 2>/dev/null)
|
|
|
|
# Extract the directive integer at the very end of the output following a colon (:)
|
|
directive=${out##*:}
|
|
# Remove the directive
|
|
out=${out%%:*}
|
|
if [[ ${directive} == "${out}" ]]; then
|
|
# There is not directive specified
|
|
directive=0
|
|
fi
|
|
__%[1]s_debug "The completion directive is: ${directive}"
|
|
__%[1]s_debug "The completions are: ${out}"
|
|
}
|
|
|
|
__%[1]s_process_completion_results() {
|
|
local shellCompDirectiveError=%[3]d
|
|
local shellCompDirectiveNoSpace=%[4]d
|
|
local shellCompDirectiveNoFileComp=%[5]d
|
|
local shellCompDirectiveFilterFileExt=%[6]d
|
|
local shellCompDirectiveFilterDirs=%[7]d
|
|
local shellCompDirectiveKeepOrder=%[8]d
|
|
|
|
if (((directive & shellCompDirectiveError) != 0)); then
|
|
# Error code. No completion.
|
|
__%[1]s_debug "Received error from custom completion go code"
|
|
return
|
|
else
|
|
if (((directive & shellCompDirectiveNoSpace) != 0)); then
|
|
if [[ $(type -t compopt) == builtin ]]; then
|
|
__%[1]s_debug "Activating no space"
|
|
compopt -o nospace
|
|
else
|
|
__%[1]s_debug "No space directive not supported in this version of bash"
|
|
fi
|
|
fi
|
|
if (((directive & shellCompDirectiveKeepOrder) != 0)); then
|
|
if [[ $(type -t compopt) == builtin ]]; then
|
|
# no sort isn't supported for bash less than < 4.4
|
|
if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then
|
|
__%[1]s_debug "No sort directive not supported in this version of bash"
|
|
else
|
|
__%[1]s_debug "Activating keep order"
|
|
compopt -o nosort
|
|
fi
|
|
else
|
|
__%[1]s_debug "No sort directive not supported in this version of bash"
|
|
fi
|
|
fi
|
|
if (((directive & shellCompDirectiveNoFileComp) != 0)); then
|
|
if [[ $(type -t compopt) == builtin ]]; then
|
|
__%[1]s_debug "Activating no file completion"
|
|
compopt +o default
|
|
else
|
|
__%[1]s_debug "No file completion directive not supported in this version of bash"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Separate activeHelp from normal completions
|
|
local completions=()
|
|
local activeHelp=()
|
|
__%[1]s_extract_activeHelp
|
|
|
|
if (((directive & shellCompDirectiveFilterFileExt) != 0)); then
|
|
# File extension filtering
|
|
local fullFilter filter filteringCmd
|
|
|
|
# Do not use quotes around the $completions variable or else newline
|
|
# characters will be kept.
|
|
for filter in ${completions[*]}; do
|
|
fullFilter+="$filter|"
|
|
done
|
|
|
|
filteringCmd="_filedir $fullFilter"
|
|
__%[1]s_debug "File filtering command: $filteringCmd"
|
|
$filteringCmd
|
|
elif (((directive & shellCompDirectiveFilterDirs) != 0)); then
|
|
# File completion for directories only
|
|
|
|
local subdir
|
|
subdir=${completions[0]}
|
|
if [[ -n $subdir ]]; then
|
|
__%[1]s_debug "Listing directories in $subdir"
|
|
pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
|
|
else
|
|
__%[1]s_debug "Listing directories in ."
|
|
_filedir -d
|
|
fi
|
|
else
|
|
__%[1]s_handle_completion_types
|
|
fi
|
|
|
|
__%[1]s_handle_special_char "$cur" :
|
|
__%[1]s_handle_special_char "$cur" =
|
|
|
|
# Print the activeHelp statements before we finish
|
|
__%[1]s_handle_activeHelp
|
|
}
|
|
|
|
__%[1]s_handle_activeHelp() {
|
|
# Print the activeHelp statements
|
|
if ((${#activeHelp[*]} != 0)); then
|
|
# Only print ActiveHelp on the second TAB press
|
|
if [ $COMP_TYPE -eq 63 ]; then
|
|
printf "\n"
|
|
printf "%%s\n" "${activeHelp[@]}"
|
|
|
|
if ((${#COMPREPLY[*]} == 0)); then
|
|
# When there are no completion choices from the program, file completion
|
|
# may kick in if the program has not disabled it; in such a case, we want
|
|
# to know if any files will match what the user typed, so that we know if
|
|
# there will be completions presented, so that we know how to handle ActiveHelp.
|
|
# To find out, we actually trigger the file completion ourselves;
|
|
# the call to _filedir will fill COMPREPLY if files match.
|
|
if (((directive & shellCompDirectiveNoFileComp) == 0)); then
|
|
__%[1]s_debug "Listing files"
|
|
_filedir
|
|
fi
|
|
fi
|
|
|
|
if ((${#COMPREPLY[*]} != 0)); then
|
|
# If there are completion choices to be shown, print a delimiter.
|
|
# Re-printing the command-line will automatically be done
|
|
# by the shell when it prints the completion choices.
|
|
printf -- "--"
|
|
else
|
|
# When there are no completion choices at all, we need
|
|
# to re-print the command-line since the shell will
|
|
# not be doing it itself.
|
|
__%[1]s_reprint_commandLine
|
|
fi
|
|
elif [ $COMP_TYPE -eq 37 ] || [ $COMP_TYPE -eq 42 ]; then
|
|
# For completion type: menu-complete/menu-complete-backward and insert-completions
|
|
# the completions are immediately inserted into the command-line, so we first
|
|
# print the activeHelp message and reprint the command-line since the shell won't.
|
|
printf "\n"
|
|
printf "%%s\n" "${activeHelp[@]}"
|
|
|
|
__%[1]s_reprint_commandLine
|
|
fi
|
|
fi
|
|
}
|
|
|
|
__%[1]s_reprint_commandLine() {
|
|
# The prompt format is only available from bash 4.4.
|
|
# We test if it is available before using it.
|
|
if (x=${PS1@P}) 2> /dev/null; then
|
|
printf "%%s" "${PS1@P}${COMP_LINE[@]}"
|
|
else
|
|
# Can't print the prompt. Just print the
|
|
# text the user had typed, it is workable enough.
|
|
printf "%%s" "${COMP_LINE[@]}"
|
|
fi
|
|
}
|
|
|
|
# Separate activeHelp lines from real completions.
|
|
# Fills the $activeHelp and $completions arrays.
|
|
__%[1]s_extract_activeHelp() {
|
|
local activeHelpMarker="%[9]s"
|
|
local endIndex=${#activeHelpMarker}
|
|
|
|
while IFS='' read -r comp; do
|
|
if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then
|
|
comp=${comp:endIndex}
|
|
__%[1]s_debug "ActiveHelp found: $comp"
|
|
if [[ -n $comp ]]; then
|
|
activeHelp+=("$comp")
|
|
fi
|
|
else
|
|
# Not an activeHelp line but a normal completion
|
|
completions+=("$comp")
|
|
fi
|
|
done <<<"${out}"
|
|
}
|
|
|
|
__%[1]s_handle_completion_types() {
|
|
__%[1]s_debug "__%[1]s_handle_completion_types: COMP_TYPE is $COMP_TYPE"
|
|
|
|
case $COMP_TYPE in
|
|
37|42)
|
|
# Type: menu-complete/menu-complete-backward and insert-completions
|
|
# If the user requested inserting one completion at a time, or all
|
|
# completions at once on the command-line we must remove the descriptions.
|
|
# https://github.com/spf13/cobra/issues/1508
|
|
local tab=$'\t' comp
|
|
while IFS='' read -r comp; do
|
|
[[ -z $comp ]] && continue
|
|
# Strip any description
|
|
comp=${comp%%%%$tab*}
|
|
# Only consider the completions that match
|
|
if [[ $comp == "$cur"* ]]; then
|
|
COMPREPLY+=("$comp")
|
|
fi
|
|
done < <(printf "%%s\n" "${completions[@]}")
|
|
;;
|
|
|
|
*)
|
|
# Type: complete (normal completion)
|
|
__%[1]s_handle_standard_completion_case
|
|
;;
|
|
esac
|
|
}
|
|
|
|
__%[1]s_handle_standard_completion_case() {
|
|
local tab=$'\t' comp
|
|
|
|
# Short circuit to optimize if we don't have descriptions
|
|
if [[ "${completions[*]}" != *$tab* ]]; then
|
|
IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur")
|
|
return 0
|
|
fi
|
|
|
|
local longest=0
|
|
local compline
|
|
# Look for the longest completion so that we can format things nicely
|
|
while IFS='' read -r compline; do
|
|
[[ -z $compline ]] && continue
|
|
# Strip any description before checking the length
|
|
comp=${compline%%%%$tab*}
|
|
# Only consider the completions that match
|
|
[[ $comp == "$cur"* ]] || continue
|
|
COMPREPLY+=("$compline")
|
|
if ((${#comp}>longest)); then
|
|
longest=${#comp}
|
|
fi
|
|
done < <(printf "%%s\n" "${completions[@]}")
|
|
|
|
# If there is a single completion left, remove the description text
|
|
if ((${#COMPREPLY[*]} == 1)); then
|
|
__%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
|
|
comp="${COMPREPLY[0]%%%%$tab*}"
|
|
__%[1]s_debug "Removed description from single completion, which is now: ${comp}"
|
|
COMPREPLY[0]=$comp
|
|
else # Format the descriptions
|
|
__%[1]s_format_comp_descriptions $longest
|
|
fi
|
|
}
|
|
|
|
__%[1]s_handle_special_char()
|
|
{
|
|
local comp="$1"
|
|
local char=$2
|
|
if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then
|
|
local word=${comp%%"${comp##*${char}}"}
|
|
local idx=${#COMPREPLY[*]}
|
|
while ((--idx >= 0)); do
|
|
COMPREPLY[idx]=${COMPREPLY[idx]#"$word"}
|
|
done
|
|
fi
|
|
}
|
|
|
|
__%[1]s_format_comp_descriptions()
|
|
{
|
|
local tab=$'\t'
|
|
local comp desc maxdesclength
|
|
local longest=$1
|
|
|
|
local i ci
|
|
for ci in ${!COMPREPLY[*]}; do
|
|
comp=${COMPREPLY[ci]}
|
|
# Properly format the description string which follows a tab character if there is one
|
|
if [[ "$comp" == *$tab* ]]; then
|
|
__%[1]s_debug "Original comp: $comp"
|
|
desc=${comp#*$tab}
|
|
comp=${comp%%%%$tab*}
|
|
|
|
# $COLUMNS stores the current shell width.
|
|
# Remove an extra 4 because we add 2 spaces and 2 parentheses.
|
|
maxdesclength=$(( COLUMNS - longest - 4 ))
|
|
|
|
# Make sure we can fit a description of at least 8 characters
|
|
# if we are to align the descriptions.
|
|
if ((maxdesclength > 8)); then
|
|
# Add the proper number of spaces to align the descriptions
|
|
for ((i = ${#comp} ; i < longest ; i++)); do
|
|
comp+=" "
|
|
done
|
|
else
|
|
# Don't pad the descriptions so we can fit more text after the completion
|
|
maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
|
|
fi
|
|
|
|
# If there is enough space for any description text,
|
|
# truncate the descriptions that are too long for the shell width
|
|
if ((maxdesclength > 0)); then
|
|
if ((${#desc} > maxdesclength)); then
|
|
desc=${desc:0:$(( maxdesclength - 1 ))}
|
|
desc+="…"
|
|
fi
|
|
comp+=" ($desc)"
|
|
fi
|
|
COMPREPLY[ci]=$comp
|
|
__%[1]s_debug "Final comp: $comp"
|
|
fi
|
|
done
|
|
}
|
|
|
|
__start_%[1]s()
|
|
{
|
|
local cur prev words cword split
|
|
|
|
COMPREPLY=()
|
|
|
|
# Call _init_completion from the bash-completion package
|
|
# to prepare the arguments properly
|
|
if declare -F _init_completion >/dev/null 2>&1; then
|
|
_init_completion -n =: || return
|
|
else
|
|
__%[1]s_init_completion -n =: || return
|
|
fi
|
|
|
|
__%[1]s_debug
|
|
__%[1]s_debug "========= starting completion logic =========="
|
|
__%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword"
|
|
|
|
# The user could have moved the cursor backwards on the command-line.
|
|
# We need to trigger completion from the $cword location, so we need
|
|
# to truncate the command-line ($words) up to the $cword location.
|
|
words=("${words[@]:0:$cword+1}")
|
|
__%[1]s_debug "Truncated words[*]: ${words[*]},"
|
|
|
|
local out directive
|
|
__%[1]s_get_completion_results
|
|
__%[1]s_process_completion_results
|
|
}
|
|
|
|
if [[ $(type -t compopt) = "builtin" ]]; then
|
|
complete -o default -F __start_%[1]s %[1]s
|
|
else
|
|
complete -o default -o nospace -F __start_%[1]s %[1]s
|
|
fi
|
|
|
|
# ex: ts=4 sw=4 et filetype=sh
|
|
`, name, compCmd,
|
|
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
|
|
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder,
|
|
activeHelpMarker))
|
|
}
|
|
|
|
// GenBashCompletionFileV2 generates Bash completion version 2.
|
|
func (c *Command) GenBashCompletionFileV2(filename string, includeDesc bool) error {
|
|
outFile, err := os.Create(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer outFile.Close()
|
|
|
|
return c.GenBashCompletionV2(outFile, includeDesc)
|
|
}
|
|
|
|
// GenBashCompletionV2 generates Bash completion file version 2
|
|
// and writes it to the passed writer.
|
|
func (c *Command) GenBashCompletionV2(w io.Writer, includeDesc bool) error {
|
|
return c.genBashCompletion(w, includeDesc)
|
|
}
|