Override bash completion for git clone
Asked Answered
H

3

16

builtin completion

The default completion for git clone (reproduced below) gives tab completion for --* options:

_git_clone ()
{
    case "$cur" in
    --*)
        __gitcomp_builtin clone
        return
        ;;
    esac
}

bash-completion 1.x (old bash)

(for a concrete instance, macos high sierra + brew installed bash-completion / git)

In the bash-completion 1.x world, to override this I would (in .bashrc / .bash_profile) define my own _git_clone completion function:

# https://github.com/scop/bash-completion/blob/d2f14a7/bash_completion#L498
__ltrim_colon_completions() {
    if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
        # Remove colon-word prefix from COMPREPLY items
        local colon_word=${1%"${1##*:}"}
        local i=${#COMPREPLY[*]}
        while [[ $((--i)) -ge 0 ]]; do
            COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
        done
    fi
}


_git_clone() {
    case "$cur" in
    --*)
        __gitcomp_builtin clone
        return
        ;;
    *)
        argc=0
        for word in "${words[@]}"; do
            case "$word" in
            git|clone|--*)
                continue
                ;;
            *)
                argc=$((argc + 1))
                ;;
            esac
        done

        if [ $argc -le 1 ]; then
            __gitcomp "https://github.com/git/git https://github.com/python/cpython"
            __ltrim_colon_completions "$cur"
        fi
        ;;
    esac
}

This works great:

(The sequence I typed here was git clone h<tab><tab>g<tab>)

$ git clone https://github.com/
//github.com/git/git          //github.com/python/cpython 
$ git clone https://github.com/git/git 

bash-completion 2.x

(for a concrete instance: stock ubuntu bionic (18.04))

In bash-completion 2.x, the model is flipped to a dynamically loaded configuration. This means that when git is tab completed, __load_completion fires, finds the git completion at the path it is installed and sources it.

Defining my own _git_clone completion function in a .bashrc / .bash_profile is now useless as it gets clobber by the dynamically sourced completion file.

I can define my own git completion in this directory:

local -a dirs=( ${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions )

(for example ~/.local/share/bash-completion/completions/git.bash). However this turns off all other git completion!

How do I make my custom clone tab completion work under this model (and have the default completion continue to work)?

Unacceptable solution(s):

  • Modify system packaged files: /usr/share/bash-completion/completions/git. This file is managed by apt.
Hildagarde answered 22/8, 2018 at 17:46 Comment(2)
Call __load_completion yourself, and then override like you used to?Britisher
@Britisher calling a double-underscored function feels quite fragileHildagarde
K
13

The official FAQ of bash-completion contains very interesting information.

First, if you are 100% sure your $BASH_COMPLETION_USER_DIR and $XDG_DATA_HOME environment variable are empty, what you specified in your original question is a good place to add your own bash-completion scripts:

~/.local/share/bash-completion/completions/git

To be noted .bash extension not necessary.

The fact is that bash-completion scripts are loaded thanks to the /etc/profile.d/bash_completion.sh file.

If you perform something in your .bashrc file, you would somehow break something in the loading chain.

Nevertheless, if you override existing completion function, you still need to ensure the loading order is correct. So loading first bash-completion script is mandatory to ensure everything ends successfully. You can easily perform it, adding this initial instruction at the beginning of your ~/.local/share/bash-completion/completions/git file:

# Ensures git bash-completion is loaded before overriding any function (be careful to avoid endless loop).
! complete -p git &> /dev/null && [ ${ENDLESS_LOOP_SAFEGUARD:-0} -eq 0 ] && ENDLESS_LOOP_SAFEGUARD=1 BASH_COMPLETION_USER_DIR=/dev/null  _completion_loader git

First it checks if git bash-completion has already been loaded, and then if this is not the case, all the bash-completion git definition are loaded. Edit: the ENDLESS_LOOP_SAFEGUARD trick allows to avoid endless loop when this is the first time bash completion is loading git part.

If needed, you can get the usage:

complete --help

complete: complete [-abcdefgjksuv] [-pr] [-DE] [-o option] [-A action] [-G globpat] [-W wordlist] [-F function] [-C command] [-X filterpat] [-P prefix] [-S suffix] [name ...] Specify how arguments are to be completed by Readline.

For each NAME, specify how arguments are to be completed. If no options are supplied, existing completion specifications are printed in a way that allows them to be reused as input.

Options:

-p print existing completion specifications in a reusable format -r remove a completion specification for each NAME, or, if no NAMEs are supplied, all completion specifications -D apply the completions and actions as the default for commands without any specific completion defined -E apply the completions and actions to "empty" commands -- completion attempted on a blank line

When completion is attempted, the actions are applied in the order the uppercase-letter options are listed above. The -D option takes precedence over -E.

Exit Status: Returns success unless an invalid option is supplied or an error occurs.

Then, and only then, you can define whatever you want, including your old way to override git clone bash completion:

# Ensures git bash-completion is loaded before overriding any function (be careful to avoid endless loop).
! complete -p git &> /dev/null && [ ${ENDLESS_LOOP_SAFEGUARD:-0} -eq 0 ] && ENDLESS_LOOP_SAFEGUARD=1 BASH_COMPLETION_USER_DIR=/dev/null  _completion_loader git

# https://github.com/scop/bash-completion/blob/d2f14a7/bash_completion#L498
__ltrim_colon_completions() {
    if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
        # Remove colon-word prefix from COMPREPLY items
        local colon_word=${1%"${1##*:}"}
        local i=${#COMPREPLY[*]}
        while [[ $((--i)) -ge 0 ]]; do
            COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
        done
    fi
}


_git_clone() {
    case "$cur" in
    --*)
        __gitcomp_builtin clone
        return
        ;;
    *)
        argc=0
        for word in "${words[@]}"; do
            case "$word" in
            git|clone|--*)
                continue
                ;;
            *)
                argc=$((argc + 1))
                ;;
            esac
        done

        if [ $argc -le 1 ]; then
            __gitcomp "https://github.com/git/git https://github.com/python/cpython"
            __ltrim_colon_completions "$cur"
        fi
        ;;
    esac
}

Each time you perform change and want to check result, you just need to request bash-completion reload for git:

_completion_loader git

Such a way, you will never lost your change, because you let the package files untouched; and still can enhanced any existing bash-completion with your own functions.

Edit:

About your fear with _completion_loader function => , but after having checked the source code, this function exists since commit cad3abfc7, of 2015-07-15 20:53:05 so I guess it should be kept backward-compatible, but true without guarantee. I'll edit my anwer to propose some alternatives

As alternative, this should be another way to get your own git completion definition (to put at beginning of your own script):

# Ensures git bash-completion is loaded before overriding any function
# Be careful to avoid endless loop with dedicated $ENDLESS_LOOP_SAFEGUARD environment variable.
if ! complete -p git &> /dev/null && [ ${ENDLESS_LOOP_SAFEGUARD:-0} -eq 0 ]; then
    # Trick: define $BASH_COMPLETION_USER_DIR environment variable here to ensure bash-completion rule are loaded once.
    export BASH_COMPLETION_USER_DIR=/usr/share
    complete -D git

    unset BASH_COMPLETION_USER_DIR
    ENDLESS_LOOP_SAFEGUARD=1 complete -D git
fi
Kink answered 22/1, 2019 at 10:3 Comment(16)
Have you checked that it works? To me it looks like there will be infinite recursion.Coinsurance
Yes I worked on it locally to ensure my answer is correct. I though about the infinite recursion but there is none, because _completion_loader is called if and only if git bash-completion functions are not already loaded.Hinch
But when _completion_loader is called for the first time the git bash-completion functions are not yet loaded and therefore it shall repeat the lookup and load the user defined file again, whereupon you have infinite recursion.Coinsurance
@Coinsurance you are right. I just added a trick with a dedicated ENDLESS_LOOP_SAFEGUARD variable allowing to protect from it.Hinch
Still I don't think that it works as intended. Which file will _completion_loader load when it is executed? The one that appears earlier in the look-up path, i.e. the user defined bash-completion script, thus the system-wide script will not be loaded at all.Coinsurance
On another note, this approach won't work (even after the said bugs are resolved) for somewhat older versions of bash completion (e.g. version 2.1, used on Ubuntu 16.04) where no mechanisms are provided for lazy-loading of user-specific completions.Coinsurance
This is much closer to what I want -- the only remaining bit that gets this the full distance is the _completion_loader function -- is there a way I can tease that out of complete or some such so this doesn't break when that function name changes? or is it specified somewhere that _completion_loader will always be what I want?Hildagarde
I understand your fear, but after having checked the source code, this function exists since commit cad3abfc7, of 2015-07-15 20:53:05 so I guess it should be kept backward-compatible, but true without guarantee. I'll edit my anwer to propose some alternatives.Hinch
@AnthonySottile Will you please confirm that this solution works for you at all? In my understanding having this code in ~/.local/share/bash-completion/completions/git prevents the default bash completions for git from loading, i.e. it has the same problem described in your question (However this turns off all other git completion!)Coinsurance
Now that I've tried it it doesn't work, however a small adjustment fixes it: ! complete -p git && BASH_COMPLETION_USER_DIR=/dev/null _completion_loader gitHildagarde
It is funny, I was working on an alternative close to that. Answer's update done.Hinch
@AnthonySottile Congrats! You arrived at the solution that I had in mind when I started working on my answer, but which I didn't like for a number of reasons (for example that it doesn't work for bash completion version 2.1).Coinsurance
@AnthonySottile Are you happy with this solution?Hinch
The current code in the answer hasn't been updated with the BASH_COMPLETION_USER_DIR=... bits so it doesn't work as written -- but it has gotten me to something that does work :DHildagarde
I've just edited my answer consequently (note that my alternative was added at end of it).Hinch
@AnthonySottile So, should I consider you accept my answer?Hinch
C
0

In your .bashrc / .bash_profile, you can force loading the default completions for git before redefining the completion for git clone:

if ! complete -p git &> /dev/null
then
    # Instead of hardcoding the name of the dynamic completion loader
    # function, you can obtain it by parsing the output of 'complete -p -D'
    _completion_loader git
fi

_git_clone() {
    COMPREPLY=("My own completion for 'git clone'")
}

EDIT

A lazily loadable version of the above approach (that doesn't eagerly load the default bash completions for git) follows:

if ! complete -p git &> /dev/null
then
    _my_git_clone()
    {
        COMPREPLY=("My own completion for 'git clone'")
    }

    # A placeholder for git completion that will load the real
    # completion script on first use     
    _my_git_comp_stub()
    {
        # Remove the old completion handler (which should be this very function)
        complete -r git

        # and load the git bash completion
        _completion_loader git

        # Rebind _git_clone to our own version
        eval 'function _git_clone() { _my_git_clone "$@"; }'

        # Tell the completion machinery to retry the completion attempt
        # using the updated completion handler
        return 124
    }

    # Install a lazy loading handler for git completion    
    complete -o bashdefault -o default -o nospace -F _my_git_comp_stub git
fi
Coinsurance answered 22/1, 2019 at 6:6 Comment(3)
this feels like a hack -- I assume the new lazy loading system was to prevent potentially-expensive eager loading like this? it does however work (and there's a noticeable pause at shell startup). you haven't really explained why it works or how one would go about "parsing the output of complete -p -D"Hildagarde
@AnthonySottile *how one would go about "parsing the output of complete -p -D" * That note is added just in case you run against an environment where the lazy loading function is named differently (I yet have to learn about such a setup). On the other hand there is no guarantee that the default completion handler (configured with complete -D) performs lazy loading - it may actually be a function doing trivial completion.Coinsurance
@AnthonySottile Lazy loading of bash completions was invented to reduce the start-up time spent on loading all of the completion definitions. In this case only the completions defined for git are loaded. However, since that script is quite large and results in noticeable pause at shell start-up, I added a lazily loadable version to my answer.Coinsurance
T
0

Note: sometime, you cannot "configure", but have to propose a patch.
For instance, Git 2.33 (Q3 2021) fixe the completion of git clone --rec* (as in '--recurse-submodules' or '--recursive')

See commit ca2d62b (16 Jul 2021) by Philippe Blain (phil-blain).
(Merged by Junio C Hamano -- gitster -- in commit fa8b225, 28 Jul 2021)

parse-options: don't complete option aliases by default

Signed-off-by: Philippe Blain

Since 'OPT_ALIAS' was created in 5c38742 (parse-options: don't emit , 2019-04-29, Git v2.22.0-rc1 -- merge) (parse-options: don't emit "ambiguous option" for aliases, 2019-04-29), 'git clone'(man) --git-completion-helper, which is used by the Bash completion script to list options accepted by clone (via '__gitcomp_builtin'), lists both '--recurse-submodules' and its alias '--recursive', which was not the case before since '--recursive' had the PARSE_OPT_HIDDEN flag set, and options with this flag are skipped by 'parse-options.c::show_gitcomp', which implements git --git-completion-helper.

This means that typing 'git clone --recurs<TAB>' will yield both '--recurse-submodules' and '--recursive', which is not ideal since both do the same thing, and so the completion should directly complete the canonical option.

At the point where 'show_gitcomp' is called in 'parse_options_step', 'preprocess_options' was already called in 'parse_options', so any aliases are now copies of the original options with a modified help text indicating they are aliases.

Helpfully, since 64cc539 ("parse-options: don't leak alias help messages", 2021-03-21, Git v2.32.0-rc0 -- merge listed in batch #7) these copies have the PARSE_OPT_FROM_ALIAS flag set, so check that flag early in 'show_gitcomp' and do not print them, unless the user explicitly requested that all completion be shown (by setting 'GIT_COMPLETION_SHOW_ALL').
After all, if we want to encourage the use of '--recurse-submodules' over '--recursive', we'd better just suggest the former.

The only other options alias is 'log' and friends' '--mailmap', which is an alias for '--use-mailmap', but the Bash completion helpers for these commands do not use '__gitcomp_builtin', and thus are unaffected by this change.

Test the new behavior in t9902-completion.sh.
As a side effect, this also tests the correct behavior of GIT_COMPLETION_SHOW_ALL, which was not tested before.
Note that since '__gitcomp_builtin' caches the options it shows, we need to re-source the completion script to clear that cache for the second test.

Terpsichorean answered 8/8, 2021 at 2:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.