// 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" if [[ "${#args[@]}" -gt 0 ]]; then requestComp+="$(printf " %%q" "${args[@]}")" fi lastParam=${words[$((${#words[@]}-1))]} lastChar=${lastParam:$((${#lastParam}-1)):1} __%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}" # When completing a flag with an = (e.g., %[1]s -n=) # 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_wordbreaks "$cur" __%[1]s_debug "The final COMPREPLY: $(printf "%%s\n" "${COMPREPLY[@]}")" # Print the activeHelp statements before we finish if ((${#activeHelp[*]} != 0)); then printf "\n"; printf "%%s\n" "${activeHelp[@]}" printf "\n" # 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 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 for comp in "${completions[@]}"; do [[ -z $comp ]] && continue # Strip any description comp=${comp%%%%$tab*} # Only consider the completions that match if [[ $comp == "$cur"* ]]; then COMPREPLY+=("$comp") fi done IFS=$'\n' read -ra COMPREPLY -d '' < <(printf "%%q\n" "${COMPREPLY[@]}") ;; *) # Type: complete (normal completion) __%[1]s_handle_standard_completion_case # If there is a single completion left, escape the completion if ((${#COMPREPLY[@]} == 1)); then COMPREPLY[0]="$(printf "%%q" "${COMPREPLY[0]}")" fi ;; 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 # compgen's -W option respects shell quoting, so we need to escape. local compgen_words="$(printf "%%q\n" "${completions[@]}")" # compgen appears to respect shell quoting _after_ checking whether # they have the right prefix, so we also need to quote cur. local compgen_cur="$(printf "%%q" "${cur}")" IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n'; compgen -W "${compgen_words}" -- "${compgen_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_wordbreaks() { if ((${#COMPREPLY[@]} == 0)); then return; fi local comp="$1" local i prefix for ((i=0; i < ${#comp}; i++)); do local char="${comp:$i:1}" if [[ "${COMP_WORDBREAKS}" == *"${char}"* ]]; then prefix="${comp::$i+1}" fi done if [[ -n "${prefix}" ]]; then for ((i=0; i < ${#COMPREPLY[@]}; i++)); do COMPREPLY[i]=${COMPREPLY[i]#$prefix} 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=() # Omit wordbreaks that would need to be escaped. local wordbreaks i for ((i=0; i < ${#COMP_WORDBREAKS}; i++)); do local char="${COMP_WORDBREAKS:$i:1}" if [[ $'\n\t ' == *"${char}"* ]]; then wordbreaks+="${char}" continue fi if [[ "${char}" == "$(printf "%%q" "${char}")" ]]; then wordbreaks+="${char}" continue fi done COMP_WORDBREAKS="${wordbreaks}" # 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) }