akinomyoga / ble.sh

Bash Line Editor―a line editor written in pure Bash with syntax highlighting, auto suggestions, vim modes, etc. for Bash interactive sessions.
BSD 3-Clause "New" or "Revised" License
2.53k stars 81 forks source link

[kubectl] Bash Completion Not Possible With Aliases #394

Closed georglauterbach closed 7 months ago

georglauterbach commented 7 months ago

ble version: 0.4.0-devel4+29cd8f10 Bash version: 5.2.15(1)-release (x86_64-pc-linux-gnu)

I use the alias

alias k=kubectl

(in order to save myself from writing kubectl) all the time. I have installed the Bash completion for kubectl with

kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl >/dev/null`

and it works fine. According to the official Kubernetes wiki, an alias can be created and completed with

alias k=kubectl
complete -o default -F __start_kubectl k

When ble.sh is not sourceed, this works fine as well. As soon as I source ble.sh, in my case with

source "${PATH_TO_BLESH}" --attach=none

and later attach, the alias stops working.

akinomyoga commented 7 months ago

This is a duplicate of #375. Maybe I should support it without any adjustments.

kubectl seems to use cobra to generate its completion, but kubectl seems to modify the result of the cobra completion. ble.sh already does dynamic adjustments to the cobra completions to make it work with aliases, but maybe kubectl's modification to the completion code breaks it.

$ kubectl completion bash
georglauterbach commented 7 months ago

I should have searched the issue tracker more thoroughly, I am sorry. Here is what you asked for:

kubectl completion bash ```bash # Copyright 2016 The Kubernetes 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. # bash completion V2 for kubectl -*- shell-script -*- __kubectl_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. __kubectl_init_completion() { COMPREPLY=() _get_comp_words_by_ref "$@" cur prev words cword } # This function calls the kubectl program to obtain the completion # results and the directive. It fills the 'out' and 'directive' vars. __kubectl_get_completion_results() { local requestComp lastParam lastChar args # Prepare the command to request completions for the program. # Calling ${words[0]} instead of directly kubectl allows to handle aliases args=("${words[@]:1}") requestComp="${words[0]} __complete ${args[*]}" lastParam=${words[$((${#words[@]}-1))]} lastChar=${lastParam:$((${#lastParam}-1)):1} __kubectl_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. __kubectl_debug "Adding extra empty parameter" requestComp="${requestComp} ''" fi # When completing a flag with an = (e.g., kubectl -n=) # bash focuses on the part after the =, so we need to remove # the flag part from $cur if [[ ${cur} == -*=* ]]; then cur="${cur#*=}" fi __kubectl_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 __kubectl_debug "The completion directive is: ${directive}" __kubectl_debug "The completions are: ${out}" } __kubectl_process_completion_results() { local shellCompDirectiveError=1 local shellCompDirectiveNoSpace=2 local shellCompDirectiveNoFileComp=4 local shellCompDirectiveFilterFileExt=8 local shellCompDirectiveFilterDirs=16 local shellCompDirectiveKeepOrder=32 if (((directive & shellCompDirectiveError) != 0)); then # Error code. No completion. __kubectl_debug "Received error from custom completion go code" return else if (((directive & shellCompDirectiveNoSpace) != 0)); then if [[ $(type -t compopt) == builtin ]]; then __kubectl_debug "Activating no space" compopt -o nospace else __kubectl_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 __kubectl_debug "No sort directive not supported in this version of bash" else __kubectl_debug "Activating keep order" compopt -o nosort fi else __kubectl_debug "No sort directive not supported in this version of bash" fi fi if (((directive & shellCompDirectiveNoFileComp) != 0)); then if [[ $(type -t compopt) == builtin ]]; then __kubectl_debug "Activating no file completion" compopt +o default else __kubectl_debug "No file completion directive not supported in this version of bash" fi fi fi # Separate activeHelp from normal completions local completions=() local activeHelp=() __kubectl_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" __kubectl_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 __kubectl_debug "Listing directories in $subdir" pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return else __kubectl_debug "Listing directories in ." _filedir -d fi else __kubectl_handle_completion_types fi __kubectl_handle_special_char "$cur" : __kubectl_handle_special_char "$cur" = # 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. __kubectl_extract_activeHelp() { local activeHelpMarker="_activeHelp_ " local endIndex=${#activeHelpMarker} while IFS='' read -r comp; do if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then comp=${comp:endIndex} __kubectl_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}" } __kubectl_handle_completion_types() { __kubectl_debug "__kubectl_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) __kubectl_handle_standard_completion_case ;; esac } __kubectl_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 __kubectl_debug "COMPREPLY[0]: ${COMPREPLY[0]}" comp="${COMPREPLY[0]%%$tab*}" __kubectl_debug "Removed description from single completion, which is now: ${comp}" COMPREPLY[0]=$comp else # Format the descriptions __kubectl_format_comp_descriptions $longest fi } __kubectl_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 } __kubectl_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 __kubectl_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 __kubectl_debug "Final comp: $comp" fi done } __start_kubectl() { 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 __kubectl_init_completion -n =: || return fi __kubectl_debug __kubectl_debug "========= starting completion logic ==========" __kubectl_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}") __kubectl_debug "Truncated words[*]: ${words[*]}," local out directive __kubectl_get_completion_results __kubectl_process_completion_results } if [[ $(type -t compopt) = "builtin" ]]; then complete -o default -F __start_kubectl kubectl else complete -o default -o nospace -F __start_kubectl kubectl fi # ex: ts=4 sw=4 et filetype=sh ```

I can also confirm that shopt -s progcomp_alias works, as described in #375.

akinomyoga commented 7 months ago

I could reproduce the problem (with a mock kubectl command). I tried to investigate what is happening here, but I now suspect a bug of Bash. In the plain Bash (without ble.sh), I observe the following behavior:

$ alias e='echo ehllo'
$ (eval e&)
hello
$ (true && eval e&)
bash: line 5: e: command not found
$ (true && { true; eval e & })
hello
$ (true && { eval e & })
hello

It is strange that the behavior of alias expansion changes by the presence of the condition true &&, and it is also strange that the behavior is only observed when eval e & is directly specified after &&. I observe the same behavior in all the currently available Bash versions 1.14 to 5.2 and the devel branch. I also tested other shells (including yash, zsh, ksh93+u, and mksh), but I couldn't find this strange behavior in any other shells.

I tried to find any relevant description in Bash Reference Manual, but I couldn't find it, though I haven't carefully checked all of the descriptions in this long manual. The only description that might be related and that I could find is that the alias expansion is only performed in interactive shells by default. What defines the interactive shells is ambiguous in Bash; (Bash internally has an "interactive" switch that frequently changes by the context even in the same interactive session).

georglauterbach commented 7 months ago

Very interesting, I didn't know that! Thank you for investigating this. What do you think, what's the best way of going forward? I can live with shopt-s ....

akinomyoga commented 7 months ago

I'll later add a workaround to ble.sh.

I'm now investigating further in the Bash source code. It seems that expand_aliases becomes somehow 0 in true && xxxx &. This can also be confirmed in the following commands:

$ (shopt -p expand_aliases)
shopt -s expand_aliases
$ (true && shopt -p expand_aliases)
shopt -s expand_aliases
$ (true && shopt -p expand_aliases &)
shopt -u expand_aliases
$ (true && { shopt -p expand_aliases & })
shopt -s expand_aliases

We observe that expand_aliases is unset only in the third case.

georglauterbach commented 7 months ago

This really is rather weird, and it indicates a bug in Bash to me as well; it least it is totally unexpected behavior.

akinomyoga commented 7 months ago

Thanks for the reply. I actually found a code comment in Bash's source code (execute_cmd.c:1583):

      ois = interactive_shell;
      interactive_shell = 0;
      /* This test is to prevent alias expansion by interactive shells that
         run `(command) &' but to allow scripts that have enabled alias
         expansion with `shopt -s expand_alias' to continue to expand
         aliases. */
      if (ois != interactive_shell)
        expand_aliases = expaliases_flag = 0;
    }

This implies that Bash's behavior for the alias expansions in (command) & is somehow an intended one. The following is an additional test cases (I found after seeing the above code comment):

$ (shopt -p expand_aliases &)
shopt -s expand_aliases
$ ({ shopt -p expand_aliases; } &)
shopt -u expand_aliases
$ ((shopt -p expand_aliases) &)
shopt -u expand_aliases

I still feel it strange that the alias expansion is performed for cmd &, while it is not performed for (cmd) &. The other shells do not behave in that way.

akinomyoga commented 7 months ago

I added a fix in commit 67e2d1ab3afa28edc34d6ffae8ff1e861ede3337. Could you update ble.sh by running ble-update and check if the problem is fixed (without the user's manual adjustment of shopt -s progcomp_alias)?

Note: Actually, commit 67e2d1ab3afa28edc34d6ffae8ff1e861ede3337 is a fix for another bug that I found in the same part. Although that bug didn't affect the completion behavior reported here, the fix to the bug also works around the problem of Bash's strange behavior.

georglauterbach commented 7 months ago

I ran ble-update and I am now on ble.sh, version 0.4.0-devel4+50d6f1bb (noarch), but without shopt -s progcomp_alias there is no proper completion for the alias k.

akinomyoga commented 7 months ago

Thank you for checking.

but without shopt -s progcomp_alias there is no proper completion for the alias k.

Does that happen even with the following setting instructed by kubectl?

complete -o default -F __start_kubectl k
georglauterbach commented 7 months ago

With

complete -o default -F __start_kubectl k

it works, nice! The shopt -s progcomp_alias is not required anymore.

akinomyoga commented 7 months ago

Thanks for the confirmation.

OK, then that's intended. With neither of complete -o default -F __start_kubectl k or shopt -s progcomop_alias, there is nothing to instruct the shell to complete k and Bash indeed does nothing with that setup. So it is natural for ble.sh not to complete anything with that setup too.

For shopt -s progcomp_alias, I actually recommend setting it. I added the fix this time to make completion settings for plain Bash also work with ble.sh, but turning on progcomp_alias enables completion of more aliases without additional settings.

georglauterbach commented 7 months ago

I understand, that's good to know. I'll add it to my setup then! This issue can be closed now I guess :)

akinomyoga commented 7 months ago

Thanks again for the report!