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