git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cobra/bash_completionsV2.go (about) 1 // Copyright 2013-2022 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 package cobra 16 17 import ( 18 "bytes" 19 "fmt" 20 "io" 21 "os" 22 ) 23 24 func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error { 25 buf := new(bytes.Buffer) 26 genBashComp(buf, c.Name(), includeDesc) 27 _, err := buf.WriteTo(w) 28 return err 29 } 30 31 func genBashComp(buf io.StringWriter, name string, includeDesc bool) { 32 compCmd := ShellCompRequestCmd 33 if !includeDesc { 34 compCmd = ShellCompNoDescRequestCmd 35 } 36 37 WriteStringAndCheck(buf, fmt.Sprintf(`# bash completion V2 for %-36[1]s -*- shell-script -*- 38 39 __%[1]s_debug() 40 { 41 if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then 42 echo "$*" >> "${BASH_COMP_DEBUG_FILE}" 43 fi 44 } 45 46 # Macs have bash3 for which the bash-completion package doesn't include 47 # _init_completion. This is a minimal version of that function. 48 __%[1]s_init_completion() 49 { 50 COMPREPLY=() 51 _get_comp_words_by_ref "$@" cur prev words cword 52 } 53 54 # This function calls the %[1]s program to obtain the completion 55 # results and the directive. It fills the 'out' and 'directive' vars. 56 __%[1]s_get_completion_results() { 57 local requestComp lastParam lastChar args 58 59 # Prepare the command to request completions for the program. 60 # Calling ${words[0]} instead of directly %[1]s allows to handle aliases 61 args=("${words[@]:1}") 62 requestComp="${words[0]} %[2]s ${args[*]}" 63 64 lastParam=${words[$((${#words[@]}-1))]} 65 lastChar=${lastParam:$((${#lastParam}-1)):1} 66 __%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}" 67 68 if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then 69 # If the last parameter is complete (there is a space following it) 70 # We add an extra empty parameter so we can indicate this to the go method. 71 __%[1]s_debug "Adding extra empty parameter" 72 requestComp="${requestComp} ''" 73 fi 74 75 # When completing a flag with an = (e.g., %[1]s -n=<TAB>) 76 # bash focuses on the part after the =, so we need to remove 77 # the flag part from $cur 78 if [[ "${cur}" == -*=* ]]; then 79 cur="${cur#*=}" 80 fi 81 82 __%[1]s_debug "Calling ${requestComp}" 83 # Use eval to handle any environment variables and such 84 out=$(eval "${requestComp}" 2>/dev/null) 85 86 # Extract the directive integer at the very end of the output following a colon (:) 87 directive=${out##*:} 88 # Remove the directive 89 out=${out%%:*} 90 if [ "${directive}" = "${out}" ]; then 91 # There is not directive specified 92 directive=0 93 fi 94 __%[1]s_debug "The completion directive is: ${directive}" 95 __%[1]s_debug "The completions are: ${out}" 96 } 97 98 __%[1]s_process_completion_results() { 99 local shellCompDirectiveError=%[3]d 100 local shellCompDirectiveNoSpace=%[4]d 101 local shellCompDirectiveNoFileComp=%[5]d 102 local shellCompDirectiveFilterFileExt=%[6]d 103 local shellCompDirectiveFilterDirs=%[7]d 104 105 if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then 106 # Error code. No completion. 107 __%[1]s_debug "Received error from custom completion go code" 108 return 109 else 110 if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 111 if [[ $(type -t compopt) = "builtin" ]]; then 112 __%[1]s_debug "Activating no space" 113 compopt -o nospace 114 else 115 __%[1]s_debug "No space directive not supported in this version of bash" 116 fi 117 fi 118 if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 119 if [[ $(type -t compopt) = "builtin" ]]; then 120 __%[1]s_debug "Activating no file completion" 121 compopt +o default 122 else 123 __%[1]s_debug "No file completion directive not supported in this version of bash" 124 fi 125 fi 126 fi 127 128 # Separate activeHelp from normal completions 129 local completions=() 130 local activeHelp=() 131 __%[1]s_extract_activeHelp 132 133 if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 134 # File extension filtering 135 local fullFilter filter filteringCmd 136 137 # Do not use quotes around the $completions variable or else newline 138 # characters will be kept. 139 for filter in ${completions[*]}; do 140 fullFilter+="$filter|" 141 done 142 143 filteringCmd="_filedir $fullFilter" 144 __%[1]s_debug "File filtering command: $filteringCmd" 145 $filteringCmd 146 elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 147 # File completion for directories only 148 149 # Use printf to strip any trailing newline 150 local subdir 151 subdir=$(printf "%%s" "${completions[0]}") 152 if [ -n "$subdir" ]; then 153 __%[1]s_debug "Listing directories in $subdir" 154 pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return 155 else 156 __%[1]s_debug "Listing directories in ." 157 _filedir -d 158 fi 159 else 160 __%[1]s_handle_completion_types 161 fi 162 163 __%[1]s_handle_special_char "$cur" : 164 __%[1]s_handle_special_char "$cur" = 165 166 # Print the activeHelp statements before we finish 167 if [ ${#activeHelp[*]} -ne 0 ]; then 168 printf "\n"; 169 printf "%%s\n" "${activeHelp[@]}" 170 printf "\n" 171 172 # The prompt format is only available from bash 4.4. 173 # We test if it is available before using it. 174 if (x=${PS1@P}) 2> /dev/null; then 175 printf "%%s" "${PS1@P}${COMP_LINE[@]}" 176 else 177 # Can't print the prompt. Just print the 178 # text the user had typed, it is workable enough. 179 printf "%%s" "${COMP_LINE[@]}" 180 fi 181 fi 182 } 183 184 # Separate activeHelp lines from real completions. 185 # Fills the $activeHelp and $completions arrays. 186 __%[1]s_extract_activeHelp() { 187 local activeHelpMarker="%[8]s" 188 local endIndex=${#activeHelpMarker} 189 190 while IFS='' read -r comp; do 191 if [ "${comp:0:endIndex}" = "$activeHelpMarker" ]; then 192 comp=${comp:endIndex} 193 __%[1]s_debug "ActiveHelp found: $comp" 194 if [ -n "$comp" ]; then 195 activeHelp+=("$comp") 196 fi 197 else 198 # Not an activeHelp line but a normal completion 199 completions+=("$comp") 200 fi 201 done < <(printf "%%s\n" "${out}") 202 } 203 204 __%[1]s_handle_completion_types() { 205 __%[1]s_debug "__%[1]s_handle_completion_types: COMP_TYPE is $COMP_TYPE" 206 207 case $COMP_TYPE in 208 37|42) 209 # Type: menu-complete/menu-complete-backward and insert-completions 210 # If the user requested inserting one completion at a time, or all 211 # completions at once on the command-line we must remove the descriptions. 212 # https://github.com/spf13/cobra/issues/1508 213 local tab=$'\t' comp 214 while IFS='' read -r comp; do 215 [[ -z $comp ]] && continue 216 # Strip any description 217 comp=${comp%%%%$tab*} 218 # Only consider the completions that match 219 if [[ $comp == "$cur"* ]]; then 220 COMPREPLY+=("$comp") 221 fi 222 done < <(printf "%%s\n" "${completions[@]}") 223 ;; 224 225 *) 226 # Type: complete (normal completion) 227 __%[1]s_handle_standard_completion_case 228 ;; 229 esac 230 } 231 232 __%[1]s_handle_standard_completion_case() { 233 local tab=$'\t' comp 234 235 # Short circuit to optimize if we don't have descriptions 236 if [[ "${completions[*]}" != *$tab* ]]; then 237 IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") 238 return 0 239 fi 240 241 local longest=0 242 local compline 243 # Look for the longest completion so that we can format things nicely 244 while IFS='' read -r compline; do 245 [[ -z $compline ]] && continue 246 # Strip any description before checking the length 247 comp=${compline%%%%$tab*} 248 # Only consider the completions that match 249 [[ $comp == "$cur"* ]] || continue 250 COMPREPLY+=("$compline") 251 if ((${#comp}>longest)); then 252 longest=${#comp} 253 fi 254 done < <(printf "%%s\n" "${completions[@]}") 255 256 # If there is a single completion left, remove the description text 257 if [ ${#COMPREPLY[*]} -eq 1 ]; then 258 __%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}" 259 comp="${COMPREPLY[0]%%%%$tab*}" 260 __%[1]s_debug "Removed description from single completion, which is now: ${comp}" 261 COMPREPLY[0]=$comp 262 else # Format the descriptions 263 __%[1]s_format_comp_descriptions $longest 264 fi 265 } 266 267 __%[1]s_handle_special_char() 268 { 269 local comp="$1" 270 local char=$2 271 if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then 272 local word=${comp%%"${comp##*${char}}"} 273 local idx=${#COMPREPLY[*]} 274 while [[ $((--idx)) -ge 0 ]]; do 275 COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"} 276 done 277 fi 278 } 279 280 __%[1]s_format_comp_descriptions() 281 { 282 local tab=$'\t' 283 local comp desc maxdesclength 284 local longest=$1 285 286 local i ci 287 for ci in ${!COMPREPLY[*]}; do 288 comp=${COMPREPLY[ci]} 289 # Properly format the description string which follows a tab character if there is one 290 if [[ "$comp" == *$tab* ]]; then 291 __%[1]s_debug "Original comp: $comp" 292 desc=${comp#*$tab} 293 comp=${comp%%%%$tab*} 294 295 # $COLUMNS stores the current shell width. 296 # Remove an extra 4 because we add 2 spaces and 2 parentheses. 297 maxdesclength=$(( COLUMNS - longest - 4 )) 298 299 # Make sure we can fit a description of at least 8 characters 300 # if we are to align the descriptions. 301 if [[ $maxdesclength -gt 8 ]]; then 302 # Add the proper number of spaces to align the descriptions 303 for ((i = ${#comp} ; i < longest ; i++)); do 304 comp+=" " 305 done 306 else 307 # Don't pad the descriptions so we can fit more text after the completion 308 maxdesclength=$(( COLUMNS - ${#comp} - 4 )) 309 fi 310 311 # If there is enough space for any description text, 312 # truncate the descriptions that are too long for the shell width 313 if [ $maxdesclength -gt 0 ]; then 314 if [ ${#desc} -gt $maxdesclength ]; then 315 desc=${desc:0:$(( maxdesclength - 1 ))} 316 desc+="…" 317 fi 318 comp+=" ($desc)" 319 fi 320 COMPREPLY[ci]=$comp 321 __%[1]s_debug "Final comp: $comp" 322 fi 323 done 324 } 325 326 __start_%[1]s() 327 { 328 local cur prev words cword split 329 330 COMPREPLY=() 331 332 # Call _init_completion from the bash-completion package 333 # to prepare the arguments properly 334 if declare -F _init_completion >/dev/null 2>&1; then 335 _init_completion -n "=:" || return 336 else 337 __%[1]s_init_completion -n "=:" || return 338 fi 339 340 __%[1]s_debug 341 __%[1]s_debug "========= starting completion logic ==========" 342 __%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" 343 344 # The user could have moved the cursor backwards on the command-line. 345 # We need to trigger completion from the $cword location, so we need 346 # to truncate the command-line ($words) up to the $cword location. 347 words=("${words[@]:0:$cword+1}") 348 __%[1]s_debug "Truncated words[*]: ${words[*]}," 349 350 local out directive 351 __%[1]s_get_completion_results 352 __%[1]s_process_completion_results 353 } 354 355 if [[ $(type -t compopt) = "builtin" ]]; then 356 complete -o default -F __start_%[1]s %[1]s 357 else 358 complete -o default -o nospace -F __start_%[1]s %[1]s 359 fi 360 361 # ex: ts=4 sw=4 et filetype=sh 362 `, name, compCmd, 363 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, 364 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, 365 activeHelpMarker)) 366 } 367 368 // GenBashCompletionFileV2 generates Bash completion version 2. 369 func (c *Command) GenBashCompletionFileV2(filename string, includeDesc bool) error { 370 outFile, err := os.Create(filename) 371 if err != nil { 372 return err 373 } 374 defer outFile.Close() 375 376 return c.GenBashCompletionV2(outFile, includeDesc) 377 } 378 379 // GenBashCompletionV2 generates Bash completion file version 2 380 // and writes it to the passed writer. 381 func (c *Command) GenBashCompletionV2(w io.Writer, includeDesc bool) error { 382 return c.genBashCompletion(w, includeDesc) 383 }