# bash completion V2 for skate -*- shell-script -*- __skate_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. __skate_init_completion() { COMPREPLY=() _get_comp_words_by_ref "$@" cur prev words cword } # This function calls the skate program to obtain the completion # results and the directive. It fills the 'out' and 'directive' vars. __skate_get_completion_results() { local requestComp lastParam lastChar args # Prepare the command to request completions for the program. # Calling ${words[0]} instead of directly skate allows to handle aliases args=("${words[@]:1}") requestComp="${words[0]} __complete ${args[*]}" lastParam=${words[$((${#words[@]}-1))]} lastChar=${lastParam:$((${#lastParam}-1)):1} __skate_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. __skate_debug "Adding extra empty parameter" requestComp="${requestComp} ''" fi # When completing a flag with an = (e.g., skate -n=) # bash focuses on the part after the =, so we need to remove # the flag part from $cur if [[ "${cur}" == -*=* ]]; then cur="${cur#*=}" fi __skate_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 __skate_debug "The completion directive is: ${directive}" __skate_debug "The completions are: ${out}" } __skate_process_completion_results() { local shellCompDirectiveError=1 local shellCompDirectiveNoSpace=2 local shellCompDirectiveNoFileComp=4 local shellCompDirectiveFilterFileExt=8 local shellCompDirectiveFilterDirs=16 if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then # Error code. No completion. __skate_debug "Received error from custom completion go code" return else if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then if [[ $(type -t compopt) = "builtin" ]]; then __skate_debug "Activating no space" compopt -o nospace else __skate_debug "No space directive not supported in this version of bash" fi fi if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then if [[ $(type -t compopt) = "builtin" ]]; then __skate_debug "Activating no file completion" compopt +o default else __skate_debug "No file completion directive not supported in this version of bash" fi fi fi # Separate activeHelp from normal completions local completions=() local activeHelp=() __skate_extract_activeHelp if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 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" __skate_debug "File filtering command: $filteringCmd" $filteringCmd elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then # File completion for directories only # Use printf to strip any trailing newline local subdir subdir=$(printf "%s" "${completions[0]}") if [ -n "$subdir" ]; then __skate_debug "Listing directories in $subdir" pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return else __skate_debug "Listing directories in ." _filedir -d fi else __skate_handle_completion_types fi __skate_handle_special_char "$cur" : __skate_handle_special_char "$cur" = # Print the activeHelp statements before we finish if [ ${#activeHelp} -ne 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. __skate_extract_activeHelp() { local activeHelpMarker="_activeHelp_ " local endIndex=${#activeHelpMarker} while IFS='' read -r comp; do if [ "${comp:0:endIndex}" = "$activeHelpMarker" ]; then comp=${comp:endIndex} __skate_debug "ActiveHelp found: $comp" if [ -n "$comp" ]; then activeHelp+=("$comp") fi else # Not an activeHelp line but a normal completion completions+=("$comp") fi done < <(printf "%s\n" "${out}") } __skate_handle_completion_types() { __skate_debug "__skate_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) __skate_handle_standard_completion_case ;; esac } __skate_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[*]} -eq 1 ]; then __skate_debug "COMPREPLY[0]: ${COMPREPLY[0]}" comp="${COMPREPLY[0]%%$tab*}" __skate_debug "Removed description from single completion, which is now: ${comp}" COMPREPLY[0]=$comp else # Format the descriptions __skate_format_comp_descriptions $longest fi } __skate_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)) -ge 0 ]]; do COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"} done fi } __skate_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 __skate_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 -gt 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 -gt 0 ]; then if [ ${#desc} -gt $maxdesclength ]; then desc=${desc:0:$(( maxdesclength - 1 ))} desc+="…" fi comp+=" ($desc)" fi COMPREPLY[ci]=$comp __skate_debug "Final comp: $comp" fi done } __start_skate() { 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 __skate_init_completion -n "=:" || return fi __skate_debug __skate_debug "========= starting completion logic ==========" __skate_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}") __skate_debug "Truncated words[*]: ${words[*]}," local out directive __skate_get_completion_results __skate_process_completion_results } if [[ $(type -t compopt) = "builtin" ]]; then complete -o default -F __start_skate skate else complete -o default -o nospace -F __start_skate skate fi # ex: ts=4 sw=4 et filetype=sh