Bash completion for git branches broken after upgrading to 21.10

I can't remember where I got this bit of script from, but my .bashrc contains the following lines:

# set up autocomplete for git aliases
if [ -f "/usr/share/bash-completion/completions/git" ]; then
  source /usr/share/bash-completion/completions/git
  __git_complete gc _git_checkout
  __git_complete gp _git_pull
else
  echo "Error loading git completions"
fi

git_branch() {
  git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/'
}

gc and gp are aliases for git checkout and git pull respectively.

I have a /usr/share/bash-completion-completions/git so when I source ~/.bashrc there is nothing echoed to the command line.

Prior to upgrading to 21.10 (21.04, 20.04, and 18.04 worked fine), I could gc feat<tab><tab> and get a list of branches starting with feat, but now I get a weird error after each <tab>:

$ gc featbash: [: -lt: unary operator expected   // first tab after 'gc feat'
bash: [: : integer expression expected
bash: [: -lt: unary operator expected
bash: [: -lt: unary operator expected
ure/bash: [: -lt: unary operator expected        // second tab after 'ure/' (this was returned by the first tab)
bash: [: : integer expression expected
bash: [: -lt: unary operator expected
bash: [: -lt: unary operator expected            // third tab after this line
bash: [: -lt: unary operator expected
bash: [: : integer expression expected
bash: [: -lt: unary operator expected
bash: [: -lt: unary operator expected

Display all 136 possibilities? (y or n)

Any idea what is causing this?


UPDATE 1:

I only get this error when using the aliases - completion using the full command git checkout feat<tab> works fine. When I vim ~/.bash_aliases it prompted me to recover a previous version and delete the swap file. This I did and everything looks fine, but I still get the error.


UPDATE 2:

Tried upgrading, then removing/reinstalling git and bash-completion to no avail.

bash-completion is already the newest version (1:2.11-2ubuntu1).
git is already the newest version (1:2.32.0-1ubuntu1).

UPDATE 3:

Did a set -xv to enable verbose/debug. Below is not the full dump but includes the portion where the messages are created:

$ gc feat+ __git_func_wrap _git_checkout
+ local cur words cword prev
+ _get_comp_words_by_ref -n =: cur words cword prev
+ local exclude flag i OPTIND=1
+ words=()
+ local cur cword words
+ upargs=()
+ upvars=()
+ local upargs upvars vcur vcword vprev vwords
+ getopts c:i:n:p:w: flag -n =: cur words cword prev
+ case $flag in
+ exclude==:
+ getopts c:i:n:p:w: flag -n =: cur words cword prev
+ [[ 6 -ge 3 ]]
+ case ${!OPTIND} in
+ vcur=cur
+ (( OPTIND += 1 ))
+ [[ 6 -ge 4 ]]
+ case ${!OPTIND} in
+ vwords=words
+ (( OPTIND += 1 ))
+ [[ 6 -ge 5 ]]
+ case ${!OPTIND} in
+ vcword=cword
+ (( OPTIND += 1 ))
+ [[ 6 -ge 6 ]]
+ case ${!OPTIND} in
+ vprev=prev
+ (( OPTIND += 1 ))
+ [[ 6 -ge 7 ]]
+ __get_cword_at_cursor_by_ref =: words cword cur
+ words=()
+ local cword words
+ __reassemble_comp_words_by_ref =: words cword
+ local exclude i j line ref
+ [[ -n =: ]]
+ exclude='[=:]'
+ printf -v cword %s 1
+ [[ -v exclude ]]
+ line='gc feat'
+ (( i = 0, j = 0 ))
+ (( i < 2 ))
+ [[ 0 -gt 0 ]]
+ ref='words[0]'
+ printf -v 'words[0]' %s gc
+ line=' feat'
+ (( i == COMP_CWORD ))
+ (( i++, j++ ))
+ (( i < 2 ))
+ [[ 1 -gt 0 ]]
+ [[ feat == +([=:]) ]]
+ ref='words[1]'
+ printf -v 'words[1]' %s feat
+ line=
+ (( i == COMP_CWORD ))
+ printf -v cword %s 1
+ (( i++, j++ ))
+ (( i < 2 ))
+ (( i == COMP_CWORD ))
+ local i cur= index=7 'lead=gc feat'
+ [[ 7 -gt 0 ]]
+ [[ -n gc feat ]]
+ [[ -n gcfeat ]]
+ cur='gc feat'
+ (( i = 0 ))
+ (( i <= cword ))
+ [[ 7 -ge 2 ]]
+ [[ gc != \g\c ]]
+ (( i < cword ))
+ local old_size=7
+ cur=' feat'
+ local new_size=5
+ (( index -= old_size - new_size ))
+ (( ++i ))
+ (( i <= cword ))
+ [[ 5 -ge 4 ]]
+ [[  fea != \f\e\a\t ]]
+ cur=feat
+ (( index > 0 ))
+ (( index-- ))
+ [[ 4 -ge 4 ]]
+ [[ feat != \f\e\a\t ]]
+ (( i < cword ))
+ (( ++i ))
+ (( i <= cword ))
+ [[ -n feat ]]
+ [[ ! -n feat ]]
+ (( index < 0 ))
+ local words cword cur
+ _upvars -a2 words gc feat -v cword 1 -v cur feat
+ (( 10 ))
+ (( 10 ))
+ case $1 in
+ [[ -n 2 ]]
+ printf %d 2
+ [[ -n words ]]
+ unset -v words
+ eval 'words=("${@:3:2}")'
words=("${@:3:2}")
++ words=("${@:3:2}")
+ shift 4
+ (( 6 ))
+ case $1 in
+ [[ -n cword ]]
+ unset -v cword
+ eval 'cword="$3"'
cword="$3"
++ cword=1
+ shift 3
+ (( 3 ))
+ case $1 in
+ [[ -n cur ]]
+ unset -v cur
+ eval 'cur="$3"'
cur="$3"
++ cur=feat
+ shift 3
+ (( 0 ))
+ [[ -v vcur ]]
+ upvars+=("$vcur")
+ upargs+=(-v $vcur "$cur")
+ [[ -v vcword ]]
+ upvars+=("$vcword")
+ upargs+=(-v $vcword "$cword")
+ [[ -v vprev ]]
+ [[ 1 -ge 1 ]]
+ upvars+=("$vprev")
+ upargs+=(-v $vprev "${words[cword - 1]}")
+ [[ -v vwords ]]
+ upvars+=("$vwords")
+ upargs+=(-a${#words[@]} $vwords ${words+"${words[@]}"})
+ (( 4 ))
+ local cur cword prev words
+ _upvars -v cur feat -v cword 1 -v prev gc -a2 words gc feat
+ (( 13 ))
+ (( 13 ))
+ case $1 in
+ [[ -n cur ]]
+ unset -v cur
+ eval 'cur="$3"'
cur="$3"
++ cur=feat
+ shift 3
+ (( 10 ))
+ case $1 in
+ [[ -n cword ]]
+ unset -v cword
+ eval 'cword="$3"'
cword="$3"
++ cword=1
+ shift 3
+ (( 7 ))
+ case $1 in
+ [[ -n prev ]]
+ unset -v prev
+ eval 'prev="$3"'
prev="$3"
++ prev=gc
+ shift 3
+ (( 4 ))
+ case $1 in
+ [[ -n 2 ]]
+ printf %d 2
+ [[ -n words ]]
+ unset -v words
+ eval 'words=("${@:3:2}")'
words=("${@:3:2}")
++ words=("${@:3:2}")
+ shift 4
+ (( 0 ))
+ _git_checkout
+ __git_has_doubledash
+ local c=1
+ '[' 1 -lt 1 ']'
+ return 1
++ __git_checkout_default_dwim_mode
++ local last_option dwim_opt=--dwim
++ '[' '' = 1 ']'
+++ __git_find_on_cmdline --no-track
+++ local word c= show_idx
+++ test 1 -gt 1
+++ local wordlist=--no-track
+++ '[' -lt 1 ']'
bash: [: -lt: unary operator expected
++ '[' -n '' ']'
+++ __git config --type=bool checkout.guess
+++ git config --type=bool checkout.guess
++ '[' '' = false ']'
+++ __git_find_last_on_cmdline '--guess --no-guess'
+++ local word c=1 show_idx
+++ test 1 -gt 1
+++ local 'wordlist=--guess --no-guess'
+++ '[' 1 -gt '' ']'
bash: [: : integer expression expected
++ last_option=
++ case "$last_option" in
++ echo --dwim
+ local dwim_opt=--dwim
+ case "$prev" in
+ case "$cur" in
++ __git_find_on_cmdline '-b -B -d --detach --orphan'
++ local word c= show_idx
++ test 1 -gt 1
++ local 'wordlist=-b -B -d --detach --orphan'
++ '[' -lt 1 ']'
bash: [: -lt: unary operator expected
+ '[' -n '' ']'
++ __git_find_on_cmdline --track
++ local word c= show_idx
++ test 1 -gt 1
++ local wordlist=--track
++ '[' -lt 1 ']'
bash: [: -lt: unary operator expected

Note this isn't a full dump, just the bits up to and including where the error messages are generated. I tried tracking one of the error messages back through the script and found this in /usr/share/bash-completion/completions/git:

# Check whether one of the given words is present on the command line,
# and print the first word found.
#
# Usage: __git_find_on_cmdline [<option>]... "<wordlist>"
# --show-idx: Optionally show the index of the found word in the $words array.
__git_find_on_cmdline ()
{
        local word c="$__git_cmd_idx" show_idx

        while test $# -gt 1; do
                case "$1" in
                --show-idx)     show_idx=y ;;
                *)              return 1 ;;
                esac
                shift
        done
        local wordlist="$1"

        while [ $c -lt $cword ]; do
                for word in $wordlist; do
                        if [ "$word" = "${words[c]}" ]; then
                                if [ -n "${show_idx-}" ]; then
                                        echo "$c $word"
                                else
                                        echo "$word"
                                fi
                                return
                        fi
                done
                ((c++))
        done
}

Looks like maybe the line local word c="$__git_cmd_idx" show_idx is to blame, as c looks empty to the -lt comparison later on triggering the unary operator expected.

Why would this no longer work after the update?


This got posted as a bug on bash completion's GitHub. The problem has been solved in Git 2.33.0.

Until this fix has trickled down into the Ubuntu repositories, you can make a workaround as follows:

In file /usr/share/bash-completion/completions/git, change

__git_func_wrap ()
{
    local cur words cword prev

into

__git_func_wrap ()
{
    local cur words cword prev __git_cmd_idx=1